Sansan Tech Blog

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

10年以上運用されているSansan iOSアプリのすべてのSwiftコードをフォーマットした話

はじめに

こんにちは!2025年4月にSansanに中途入社し、技術本部 Sansan Engineering Unit Mobile Application GroupでiOSエンジニアとして開発に携わっているヤズジュ夢佐です。

今回は、Mobileチーム「技術負債返済」をテーマとしたTech Blogリレー企画の第六弾となります。

  1. 技術負債解消に向けた継続的運用の試み(2025-09-01)
  2. 10年もののSansan Mobileで負債・リスクに向き合う(2025-10-29)
  3. Android Edge-to-Edge対応 大規模アプリですべての画面を更新するための道のり (2025-11-13)
  4. SansanのAndroid View→Jetpack Composeへの移行計画 (2025-11-17)
  5. 10年ものプロダクトの技術負債: Realmのスレッド安全性を担保する

本記事では、2025年の5~6月に行った、Sansan iOSアプリのすべてのSwiftコードをフォーマットした話について紹介します。

当時の状況

Sansan iOSアプリは10年以上運用されており、歴史の流れの中で生み出されてきたコードのスタイルはさまざまでした。既存のコードスタイルが一貫していないだけでなく、関わっている開発メンバーも多いため、新たに生み出されるコードのスタイルも細かな差が発生します。つまり、既存のコードも新たに生み出されるコードも一貫性がない状態でした。

直面していた問題

コードスタイルの一貫性の無さから発生する問題は、開発生産性の低下です。 低下の要因は単なる可読性の低下から生まれるコードの理解の遅延だけではありません。 遅延はコードレビューにおいても発生します。

Pull Requestで提示されたコードの変更にスタイル上の問題がある場合、スタイル上の問題に対する指摘とその指摘への対応のやり取りが発生します。そのやり取り自体がコストであり、進行を遅延させる要因となります。 その上、"ここはついでに書き方を綺麗にしておこうかな"と本来必要な開発の変更に追加して行われるリファクタリングが発生した場合、その分レビュワーが見ることになる差分は増え、コードレビューは遅延します。

もし、既存のコードも新たに生み出されるコードも特定のルールに従ってスタイルが一貫していれば、可読性が高まりコードの理解が加速されるだけでなく、コードレビューも開発中の機能に集中して素早く進めることができるようになります。 この目的を叶えるためには、フォーマットツールの活用ができそうですね。

実は導入されていたフォーマットツール

実はSansan iOSには2019年の4月の時点でフォーマットツールとしてnicklockwood/SwiftFormatが導入されていました。 github.com

導入されていましたが、既存コードも新規コードもスタイルは一貫していると言えない状況でした。 原因として挙げられたのは次の通りです。

  • 既存のコードがSwiftFormatによって完全にフォーマットされていないこと
  • SwiftFormatのルールの見直しがしばらく行われておらず、ルール自体が緩いため、フォーマットをかけても個々のスタイルに差が残ること
  • 任意のタイミングでフォーマットを実行することになっていたため、フォーマット漏れが起きていたこと

コードスタイルの一貫性を実現するために

既存のコードも、今後変更・追加されていく未来のコードも、スタイルが一貫した状態を作るため、次のような流れで進めることとしました。

  1. フォーマッターに適用するルールを見直しつつ、既存コード全体にフォーマットをかける
  2. 新しく追加されるコードが必ずフォーマットされる仕組みを作る

ここからは、それぞれのステップ毎に行ったことを紹介していきます。

ステップ 1: フォーマッターに適用するルールを見直しつつ、既存コード全体にフォーマットをかける

SwiftFormatにはフォーマットの動きを決めるルールが用意されており、その数は執筆時点(2025年12月23日)で132個に及びます。

https://github.com/nicklockwood/SwiftFormat/blob/main/Rules.md

それぞれのルールは独立しており、1つ1つ有効化・無効化を切り替えることができます。 この特性を利用して、次のような流れでルールの選定を行いました。

  1. まずはすべてのルールを無効化する
  2. ルールを1つ有効化し、実際にプロジェクト全体にフォーマットをかける
  3. フォーマット結果を元に、適用するべきか判断する

ここで、気をつけなければならないのは、フォーマットのルールによっては挙動が変わってしまうことがある という点です。 コードベースは膨大であり、挙動に変化が起きる恐れのあるルールを有効にしてしまうと、思わぬ箇所で不具合を生み出してしまう可能性があります。 実際に幾つかのルールを有効にしてフォーマットすることで、Unit Testが通らなくなるケースもありました。 プロジェクト全体にフォーマットをかけると、変更はたった1つのフォーマットルールでも数百~数千行に及ぶこともあります。 挙動に変化が起きる恐れが限りなく低いかつ、フォーマット結果自体がチームにとって望ましいもののみを有効化していきました。

挙動が変わる恐れのあるルール例

実際に実行して、挙動が変わってしまうことを理由に入れない判断となったルールを一部紹介します。これ以外にも、挙動が変わったものはいくつかありました。単なる改行や空白、コメントなどの調整ではないフォーマットルールは、それ自体は問題がないように見えても、前後のコードとの関係で挙動が変わる可能性があるので、注意して精査する必要があります。

採用してよかったルール例

個人的に気に入っている好きなルールをいくつかを紹介します。

例1: docComments

classやstruct, function, var, letなどの定義の直前にくるコメントは、//ではなく///にしてくれます。 コード自体の可読性では大した変化はないですが、これを適用するとXcode上でサジェストされた時にコメントの内容が表示されたり、option + clickした時にコメント内容が表示されたりするので、コメントの露出が増えます。 これによって、重要な説明、注意事項、tipsなどがより開発者の目に入りやすくなり、コード理解の速度を速めたり、問題を未然に防ぐことができたりします。

- // A placeholder type used to demonstrate syntax rules
+ /// A placeholder type used to demonstrate syntax rules
  class Foo {
-     // This function doesn't really do anything
+     /// This function doesn't really do anything
      func bar() {
-         /// TODO: implement Foo.bar() algorithm
+         // TODO: implement Foo.bar() algorithm
      }
  }

例2: redundantOOO系

redundantとは、冗長な、余分なものという意味です。あってもなくても良いコードを綺麗に消してくれます。最高です。

redundantBreak

  switch foo {
    case bar:
        print("bar")
-       break
    default:
        print("default")
-       break
  }

redundantGet

  var foo: Int {
-   get {
-     return 5
-   }
  }

  var foo: Int {
+   return 5
  }

redundantInit

- String.init("text")
+ String("text")

redundantEquatable

  struct Foo: Equatable {
      let bar: Bar
      let baaz: Baaz

-     static func ==(lhs: Foo, rhs: Foo) -> Bool {
-         lhs.bar == rhs.bar 
-             && lhs.baaz == rhs.baaz
-     }
  }

例3: consecutiveOOO系

連続する何かを1つにしてくれるルールです。気持ち良いですね。

consecutiveBlankLines

  func foo() {
    let x = "bar"
-

    print(x)
  }

  func foo() {
    let x = "bar"

    print(x)
  }

consecutiveSpaces

- let     foo = 5
+ let foo = 5

例4: wrapArguments

arguments, parameters, collectionsを、どういったスタイルでwrapするか統一ができます。 optionが多数用意されており、チームの好みでどれを選択するか変わるので、迷うと思います。 Sansan iOSでは、GoogleのSwift Style Guideなども参照しつつ、optionを決定しました。 採用したoptionは次の通りです。

--enable wrapArguments
--wraparguments before-first
--wrapparameters before-first
--wrapcollections before-first
--wrapreturntype never
--wrapeffects never
--wrapconditions preserve
--wraptypealiases preserve

実際のコード差分の例がこちらです。絶大なインパクトですね。

-        helloworld.greetings.append(contentsOf: [.init(veryLongNameHere: "value1",
-                                                               param2: nil,
-                                                               param3: "phase1",
-                                                               veryLongNameHere2: nil,
-                                                               veryLongNameHere3: nil,
-                                                               param4: nil),
-                                                         .init(veryLongNameHere: "value2",
-                                                               param2: nil,
-                                                               param3: "phase2",
-                                                               veryLongNameHere2: nil,
-                                                               veryLongNameHere3: nil,
-                                                               param4: nil)])
-        let presenter = VeryLongClassNameHere(view: dummyView,
-                                                    router: DummyRouter(),
-                                                    interactor: stubInteractor,
-                                                    tracker: DummyTracker(),
-                                                    hogehoge: .init(rawValue: "value"))
+        helloworld.greetings.append(contentsOf: [
+            .init(
+                veryLongNameHere: "value1",
+                param2: nil,
+                param3: "phase1",
+                veryLongNameHere2: nil,
+                veryLongNameHere3: nil,
+                param4: nil
+            ),
+            .init(
+                veryLongNameHere: "value2",
+                param2: nil,
+                param3: "phase2",
+                veryLongNameHere2: nil,
+                veryLongNameHere3: nil,
+                param4: nil
+            )
+        ])
+        let presenter = VeryLongClassNameHere(
+            view: dummyView,
+            router: DummyRouter(),
+            interactor: stubInteractor,
+            tracker: DummyTracker(),
+            hogehoge: .init(rawValue: "value")
+        )

最終結果

ルールを1つ1つ慎重に検討し、既存コード全体に変更を適用していくことで、安全に大規模な変更を進めることができました。 最終的に、このフォーマットプロジェクトでは+19098行, -16430行, 1683 filesに及ぶ変更を適用し、リリース後もフォーマット起因の不具合を発生させることなく、無事に終えることができました。

ステップ 2: 新しく追加されるコードが必ずフォーマットされる仕組みの構築

新しく追加されるコードが必ずフォーマットされる仕組みを作るために、githookを利用して差分のあるswift fileをcommit時に自動でフォーマットするアプローチを採用しました。 CIでSwiftFormatを走らせ、フォーマット結果を自動でCommitさせる方法も検討しましたが、その場合はLocalとRemoteで差が生まれ、開発者がRemoteの変更をフォーマットが走るたびに取り込まなければいけなくなります。 それに対してgithookの場合は、commit時に即座にフォーマッターによるLocalに変更が反映され、それがRemoteへpushされるため、Remoteの取り込み作業が発生せず、開発者のストレスが小さいです。 しかし、この場合はフォーマットをかけたくない場合でもcommitしたら必ずフォーマットがかかってしまうという問題がありました。

SwiftFormatの実行をopt-outする

例えば、SwiftFormatにはunusedPrivateDeclarationsという使われていないprivateな変数やfunctionを削除するルールがありますが、 作業途中のcommitを行う場合、これを消されては困ります。

それ以外にも、フォーマットはfile単位で行われるため、fileの一部の行だけcommitしたい、というときに、フォーマットをするとfile全体がcommitされてしまうことも問題として上がりました。 これを解決するため、commit messageにskip formatという文字列が存在している場合は、フォーマットをskipするというopt out方式を取りました。

githookのpre-commitではcommit messageを取得できないため、workaround的ではありますが、次のような形で実現しています。

  1. githookのprepare-commit-msgでcommit messageを確認し、"skip format"がmessageに含まれていないか確認。含まれていない場合のみ2へ進む
  2. prepare-commit-msgの中でその時点でstageされているすべてのswift fileに対してSwiftFormatを実行し、git addする
  3. githookのpost-commitでフォーマットをかけたfileをamendすることでcommitにフォーマット結果を含める

これによって、開発者はcommit messageによるフォーマットのopt-outが可能かつ、普段開発している時はcommit時に自動でフォーマットが適用され、ストレスフリーな体験を実現できます。

念押しのCIチェック

skip formatという文字列がcommit messageに存在しない限り、githookで必ずフォーマットが走るようになりました。 しかし、それでもフォーマットが漏れているコードがわずかにproductionに入ってしまうという問題が発生しました。 原因としては、skip formatで一時的にcommitした差分がそのまま取り込まれてしまうケースと、チームで共有されたgithookのSetup処理が適切にされておらず、フォーマットが走らずにcommitされてしまうケースが考えられました。 これらに対策するため、GitHub ActionsでSwiftFormatによる差分が発生しないか検知をする形を取りました。 SwiftFormatはGitHub ActionsのMacOS machineにpre-installされており、かつ実行時間も数秒から十数秒程度であるため、開発体験を損なわない形で簡単にCIに導入できます。

最後に

既存のコードベースへの大規模なフォーマットの適用によってコードの可読性が向上しただけでなく、新たなコードにも自動でフォーマットが適用されることで、日々コードレビューで行われるスタイルに関する指摘や対応も大きく減少した実感があります。 この記事の取り組みが、同じ悩みを抱える開発チームの参考になれば幸いです。

Sansan技術本部ではカジュアル面談を実施しています

Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。

© Sansan, Inc.