Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

SwiftLint × Sider + SwiftFormat で Swift らしくリファクタする

はじめまして。 Sansan iOS アプリエンジニアの中川です。

私は 2014 年新卒として Join し、4 年間はサーバーサイドエンジニアでしたが、直近のスマホアプリ利用率の増加や新コンセプトを体現するためのプロダクトリニューアルでモバイルエンジニアの需要が高まったため、転向しました。

iOS アプリを開発するのは就職活動のために萌えキャラクター育成ゲームを作った以来なので、 6 年ぶりでした。余談ですが、Sansan での面接時にこのゲームのコンセプトを取締役の一人に熱弁していたのを思い出しました。今、思い返すと黒歴史でしかないですね…

今回の記事では、こんな自分が Sansan iOS アプリ開発の現場に飛び込んで、感じたこと、やってきたことをご紹介しようと思います。

コードベースに秩序がない

Sansan の iOS アプリは 2014/01/08 にバージョン 1.0.0 がリリースされ、今日まで運用保守されてきた 6 年目を迎えるアプリです。プロダクトの成長に伴い、アプリに関わるチームの体制やそのメンバーも千変万化していました。

そんな状況なので、コードベースもその時々で関わっていたメンバーが思い思いの設計やコーディングをしており、とある箇所は Cocoa MVC だったり、また別の箇所では RxSwift を利用した MVVM だったり、責務の分離が画面によって違っていたりと同じアプリなのに設計が統一されておらず、私のような新規メンバーがキャッチアップするには大変難度が高い状態になっていました。また、コーディングのフォーマットも場所によってまちまちでいざコードを書く際にどれを指針にして書いたら良いのか分からず、直近で変更されたコードのフォーマットを参考にして、頑張って書いてました。

秩序をもたらすにどうしたらよいか?

このままではプロダクトやチームの成長にコードベースがついていけなくなり、いつかは破綻してしまい、リライトする必要が出てきます。これはエンジニアとしては負債を一括で返せるので嬉しいですが、会社やプロダクトを利用するエンドユーザーにとっては長期間、新機能の提供や既存機能の改善が止まることになるので、利用率の低下につながります。ここで踏みとどまってもらえれば、良いですが、解約になってしまえば、会社としての損失になります。

そのため、運用保守しているアプリはリライトではなく、日々の開発の中での継続的なリファクタが大事になります。*1この継続的なリファクタを自然とメンバーが行えるような仕組みづくりに、今回取り組んでみました。

Sansan での事例

前置きが長くなりましたが、現在、 Sansan で行っている事例を 2 つご紹介します。各ツールの導入方法については割愛し、どのように運用しているのかに焦点を置きます。

1. SwiftLint × Sider の導入

SwiftLint は、 GitHub の Swift Style Guide に基づいて、 Swift のスタイルと規則を強制するためのツールで Realm Inc. がメンテナンスしています。 Star の数は 2019/05/23 時点で 11785 と多くの Swift を利用するエンジニアから支持を受けています。

次に Sider は、 GitHub のリポジトリ毎に複数の静的解析ツールを動かす事ができ、それぞれのリポジトリで固有の問題の見逃しを防ぎ、チーム内での情報共有を進めることができるコードレビュー自動化サービスです。

github.com

sider.review

なぜ、組み合わせて利用しているのか?

GitHub の Pull Request で適用漏れに気づけるようにするためです。これだけであれば、 CI 上で Danger と Danger の SwiftLint プラグインを利用することで Pull Request にインラインコメントを残すこともできます。しかし、 6 年間も無秩序だったコードベースの差分だけに絞って、 SwiftLint でチェックしたとしても、多くのインラインコメントがつきます。本来、 Pull Request で議論したいことはエンドユーザーに届けたい機能のビジネスロジックや今後の保守性や拡張性を担保できる設計についてであり、 Swift の書き方やドキュメントにまとめられているようなルールについてではないです。

Sider と組み合わせて使うことのメリットは以下になります。

  • Sider 上で静的解析ツールの結果を管理できるので、 Pull Request でのやり取りにノイズが乗らず、新規メンバーがキャッチアップしやすい
  • Sider のチェックに合格してから、 Pull Request のレビューをお願いするルールを引くことで不要なやり取りを減らすことができ、メンバーの時間を節約できる

SwiftLint の設定はどうしてる?

ほとんど、 realm/SwiftLint にある .swiftlint.yml のままです。変更点は以下です。

1. excluded で CocoaPods / Carthage で管理しているライブラリは除外するために追記
excluded:
+  - Pods/
+  - Carthage/
2. excluded に XCTest / XCUITest のディレクトリを除外するために追記
excluded:
+  - SansanTests/
+  - SansanUITests/
3. included / excluded にある realm/SwfitLint 固有の設定を削除
-included:
-  - Source
-  - Tests
excluded:
-  - Tests/SwiftLintFrameworkTests/Resources
4. analyzer_rules は利用しないので、設定を削除
-analyzer_rules:
-  - unused_import
-  - unused_private_declaration
5. opt_in_rules / disabled_rules を変更して、 Sansan iOS で受け入れることができないルールを無効化
opt_in_rules:
(変更のないルールについては省略)
-  - file_header
-  - number_separator
-  - prohibited_interface_builder
disabled_rules:
+  - file_header
+  - force_cast
+  - force_try
+  - number_separator
6. Sansan iOS で不要な設定を削除
-identifier_name:
-  excluded:
-    - id
-number_separator:
-  minimum_length: 5
-file_name:
-  excluded:
-    - main.swift
-    - LinuxMain.swift
-    - TestHelpers.swift
-    - shim.swift
-    - AutomaticRuleTests.generated.swift
-
-custom_rules:
-  rule_id:
-    included: Source/SwiftLintFramework/Rules/.+/\w+\.swift
-    name: Rule ID
-    message: Rule IDs must be all lowercase, snake case and not end with `rule`
-    regex: identifier:\s*("\w+_rule"|"\S*[^a-z_]\S*")
-    severity: error
-  fatal_error:
-    name: Fatal Error
-    excluded: "Tests/*"
-    message: Prefer using `queuedFatalError` over `fatalError` to avoid leaking compiler host machine paths.
-    regex: \bfatalError\b
-    match_kinds:
-      - identifier
-    severity: error
-  rule_test_function:
-    included: Tests/SwiftLintFrameworkTests/RulesTests.swift
-    name: Rule Test Function
-    message: Rule Test Function mustn't end with `rule`
-    regex: func\s*test\w+(r|R)ule\(\)
-    severity: error

Push する前に静的解析結果が知りたい

SwiftLint × Sider を導入してから、少し経った後に以下の意見があがりました。

f:id:ynakagawa33:20190525082153p:plain

SwiftLint にはビルド時に静的解析を行い、 IDE 上に警告やエラーを表示できる機能があります。導入当初はすべてのファイルにかけてしまい、ビルド時間も伸びてしまうし、本当に対応すべき、警告やエラーが埋もれてしまうことが考えられたため、控えていたのですが、 SwiftLint を変更があったファイルのみ実行すれば、ビルド時間も伸ばさずに適切な警告とエラーを表示できると思い、 Build Phrase に以下のスクリプトを追加しました。

if which "${PODS_ROOT}/SwiftLint/swiftlint" >/dev/null; then
  git diff --name-only | grep .swift | while read filename; do
    "${PODS_ROOT}/SwiftLint/swiftlint" --path "$filename" 
  done
else
  echo "SwiftLint does not exist, download from https://github.com/realm/SwiftLint"
fi

2. SwiftFormat の導入

SwiftFormat は Swift コードのフォーマットのためのコマンドラインツールです。インデントを調整すること以外に暗黙的な self を挿入または削除したり、余分なカッコを削除したりといった Swift らしくない多くのコードを修正できます。

github.com

なぜ、 SwiftFormat を選んだのか?

いわゆるコードフォーマッターは SwiftFormat 以外にもたくさんありますが、私が SwiftFormat を選択したのは以下のようなメリットがあるからです。

  • Xcode source editor extension があり、ビルドしなくてもフォーマットをかけられる
  • Git pre-commit hook でフォーマット漏れを防げる
  • 設定ファイルでメンバー同士の設定を共有できる
  • カスタマイズできる設定項目が多い

SwiftFormat の設定はどうしてる?

Sansan iOS では以下のような設定を .swiftformat に記述して、運用しています。

# format options
--allman false
--binarygrouping none
--closingparen balanced
--commas inline
--conflictmarkers reject
--decimalgrouping none
--elseposition same-line
--empty void
--exponentcase lowercase
--exponentgrouping disabled
--fractiongrouping disabled
--fragment false
--header ignore
--hexgrouping none
--hexliteralcase uppercase
--ifdef indent
--importgrouping alphabetized
--indent 4
--indentcase false
--linebreaks lf
--octalgrouping none
--operatorfunc spaced
--patternlet hoist
--ranges no-space
--self remove
--selfrequired 
--semicolons inline
--stripunusedargs closure-only
--trailingclosures 
--trimwhitespace always
--wraparguments before-first
--wrapcollections before-first
    
# file options
--exclude Pods,Carthage

exclude オプションで SwiftLint と同様に CocoaPods / Carthage で管理しているライブラリは除外しています。ここで XCTest / XCUITest のディレクトリを除外していないのはビルド時にフォーマットをかけるのではなく、 Xcode source editor extension を利用して、フォーマットをかけたい場所を選択して、フォーマットをできるようにし、メンバーがコーディングする際の補助ツールとして使いたかったからです。

コードベースに秩序はもたらせたか?

まとめ & 所感です。改善の前後でどうなったのかを提示した後に今後の展望とか語ります。

以前までは各 Swift ファイルごとに書き方が違っていたり、 Swift らしく記述されていなかったのが、 SwiftFormat により、チームで統一されたフォーマットを使って Swfit らしく記述でき、 SwfitLint × Sider で適用漏れを無くしつつ、 Pull Request では自分たちが行うべき本質的な議論をすることができるようになりました。

ですが、冒頭で取り上げた課題の一つの「設計が統一されていない」という問題は解決できていません。こちらについては進行中で、形にできたら発信したいなと考えています。

Before

f:id:ynakagawa33:20190525082229p:plain

After

f:id:ynakagawa33:20190525082259p:plain

宣伝

定番のやつ、書きます。

Sansan では 21 卒向けサマーインターンシップの募集をしています。 私がサーバーサイドエンジニアだったころにメンターをした経験があり、その時の本音を会社ブログの方で語ってます。

jp.corp-sansan.com

モバイルエンジニアも募集対象なので、 6 年目を迎えるアプリの開発に興味のある方はぜひ。

hrmos.co

*1:リライトやリファクタについての説明は下記の記事がわかりやすくまとまっているので、知らなかった方は読んでみると理解が深まります。

buildersbox.corp-sansan.com

© Sansan, Inc.