🌐

生成AIを活用したSwiftGenからXcode標準機能への移行

に公開

こんにちは、モバイルエンジニアの藤野です。

String CatalogsやAsset Symbolの登場で、SwiftGenを使わずに、Xcode標準の機能だけで型安全なアセット利用ができるようになりました。
現時点でSwiftGenがSwift 6未対応ということもあり、東急線アプリでは、SwiftGenからXcode標準に移行することにしました。

今回は置き換えにPythonスクリプトを利用して、その過程で生成AIも利用したため、そちらの解説もできればと思います。

SwiftGenとXcode標準機能の比較

SwiftGenのLocalizable Stringsに関する機能は、XcodeのString CatalogsやLocalizedStringKeyと、画像アセットや色アセットに関する機能は、Asset Symbol(ImageResource, ColorResource)とそれぞれ対応しています。
両者ともビルド時に定義を作成して、型安全に利用することができるという点で共通していますが、特にアセットについて現時点では微妙な使い勝手の違いがあります。

SwiftGenのメリット

SwiftGenではenumを利用した名前空間のような階層分けをすることができますが、Xcodeの機能の場合はすべて並列に定義されるため、グループを意識した使い方をすることができません。
また、生成された定義についても、SwiftGenではファイルが作成されるため確認することができますが、Xcodeが生成した定義は直接見ることができず、コードジャンプをしてもアセットファイルに飛ばされてしまいます。
こちらは、例えば定義をpublicにしたい場合に都合が悪い場面があります。

Xcode標準機能のメリット

Xcode標準機能のメリットとしては、やはりこれからサポートされ続けることや、Xcode自体の機能と組み合わせたさらなる拡張が期待できるという点です。
SwiftGenはSwift 6未対応ですが、諸々の対応状況やバグの有無を意識しなくて良くなることも標準機能を使う大きな利点です。

東急線アプリでは、標準機能を使うメリットの方が大きいと感じたため、SwiftGenの利用をやめて、そちらに乗り換えることにしました。
以下では、Localizable Stringsと画像アセットの対応例を紹介したいと思います。

移行フローと生成AIの活用

移行については、以下のようなフローを取ることにしました。

  • ソースコードやIBファイル内の参照を置き換える処理を行うスクリプトを作成 (生成AI利用部分)
  • スクリプトをレビュー
  • 置き換え結果の微調整
  • ファイルの削除やプロジェクトファイルの修正

生成AIにAgentモードで置き換えを直接指示した場合、一つひとつのファイルを見に行くため、膨大なファイルがある場合は処理しきれず、また、ファイルごとの作業内容のブレによりレビューコストも膨らんでしまいます。
その代わりに、テキスト処理で置き換えを行うスクリプトを作成させて、そちらの調整とコードレビューを実施することで、API制限やコンテキストの限界を超える事なく作業を完了させ、さらに実行ごとの結果のブレをなくすことができます。

また、精度面についても、イレギュラーなパターンを全て完璧にスクリプトで置き換えをさせようとすると、かなり複雑になることが予想されました。
そのため、ある程度の誤りを許容して、手作業での修正と組み合わせることで、余計なプロンプトのやり取りを避けて、全体の作業時間を短縮することができました。

完全に動くスクリプトではないこと、プロジェクトのコンテキストに依存して汎用性が低いこと、コード全体が長いことから、以下ではスクリプトのコードは割愛しますが、主要なプロンプトについては記載します。
ツールはGitHub Copilot(VSCode, Xcode)を、モデルはClaude Sonnet 4やGPT-5を利用しました。

Localizable.strings+SwiftGenからString Catalogsへの移植

東急線アプリでは、現在のところ多言語対応はされていませんが、Localizable.stringsからSwiftGenで生成したL10nクラス経由で固定文言を利用していました。
すでに多言語対応されているアプリの場合、String Catalogsへの変換にもう一工夫が必要になるかと思いますが、東急線アプリでは、

  • ソースコード内のL10n参照を、LocalizedStringKeyを介した形に変換
  • Localizable.stringsとL10n、SwiftGenの設定を削除
  • Xcodeビルド時に日本語文字列をキーとしたString Catalogsを自動生成

というフローで対応しました。
日本語文字列をキーとする理由は、多言語対応に備えつつ、ビルド時のString Catalogsの自動生成のみで管理を行うことで、普段の開発でのオーバーヘッドを少なくするという狙いがあります。
(Xcode26でString Catalogs Symbolsが登場したため、再考の余地はありそうです)

変換処理の内容

ここからは、最初の変換の部分について解説したいと思います。

"some_alert_title" = "アラートのタイトル";
"train_direction" = "%@方面";

例えばこのようなLocalizable.stringがあった場合、SwiftGenで生成されるコードは以下のようになります。

enum L10n {
    static let someAlertTitle = L10n.tr("Localizable", "some_alert_title", fallback: "アラートのタイトル")
    static func trainDirection(_ p1: Any) -> String {
        L10n.tr("Localizable", "train_direction", String(describing: p1), fallback: "%@方面")
    }
}

このコードの参照は、例えば以下のようになります。

UIAlertController(title: L10n.someAlertTitle, ...)
Text(L10n.trainDirection(direction))

String.LocalizationValueLocalizedStringKeyを介した形に変換すると、次のような形になります。

UIAlertController(title: String(localized: "アラートのタイトル"))
Text("\(direction)方面")

つまり、コードの変換内容としては、

  • L10n.someAlertTitle -> Text("\(direction)方面") (Stringとしての利用ケース)
  • L10n.trainDirection(direction) -> Text("\(direction)方面") (LocalizedStringKeyでの利用)

の2パターンで、それぞれL10nクラス内の定義のfallbackから元の文言を拾ってくることで、あとは単純な文字列置換として処理することができます。

生成AIを利用したスクリプト生成とレビュー

上記の内容を生成AIに投げてみます。GPT-5の登場前だったため、Claude Sonnet 4を中心に活用しています。
以下はプロンプトを日本語に直したものです。

SwiftGenのL10nの利用箇所を全てString Catalogsを使って置き換えるスクリプトを作成してください。例えば、
`L10n.someAlertTitle`は`String(localized: "アラートのタイトル")`のように、`L10n.trainDirection(direction)`は`Text("\(direction)方面")`のようになります。
SwiftUIの場合は、`Text(L10n.someAlertTitle)`は`Text("アラートのタイトル")`のようになります。
スクリプトの言語は任意です。

得意なスクリプト言語で作成されることを期待して、言語指定はなしにしましたが、Pythonが選ばれました。
出力されたPythonスクリプトは、以下のようなフローで処理されるようになっています。

  • L10nクラスの内容からキーと文字列の対応を表すdictionaryを作成する
  • os.walkでファイルを走査してText(L10n.key)タイプの利用箇所を正規表現で見つけて、上のdictionaryを使って置き換える
  • 同様にos.walkでファイルを走査してその他のL10n.keyタイプの利用箇所を正規表現で見つけて、上のdictionaryを使って置き換える

想定通りのフローで、コードを軽く眺めて問題点が見つからなそうだったため、そのまま利用することにしました。

スクリプトの実行結果と評価

実際に実行してみたところ、1箇所を除いて適切に置き換えることができました。
問題のあった箇所は文言の文字列に()が含まれるもので、正規表現を修正すれば解決しそうでしたが、こちらは手作業で修正しています。

普段はgit grepsedなどを組み合わせて処理しがちですが、複雑なシェルスクリプトを書くよりは見通しがよく、プロンプト入力も一度で済んだため、効率よく作業を完了することができました。
また、dry run機能も指示なしでコードに含まれていたため、その点もプラスに働きました。

画像リソースのSwiftGenからAsset Symbolへの移植

画像リソースについても、SwiftGenからXcodeのビルド時に生成される、Asset Symbolに移植しました。
こちらはBuild Settings > Generate Asset Symbolsを有効にすることで、コードとしては見えない形で自動生成されます。

SwiftGenではAsset.Imagesのような場所に定義を作成する形でしたが、Xcodeの機能ではImageResourceとして利用できるようになります。
ただし、現状のAsset Symbolの機能では、先述のようにenumを使った階層化ができないため、各ImageResourceは横並びになります。
そのため、もし、定義されたアセット名が抽象的だったり、似たような名前のものがあったりすると利用する際に混乱が生じることになります。

この問題を解消するために、前処理として各アセットのリネームを挟むことにしました。
例えば、SomeFeature/background.imagesetのようなディレクトリで区切られたリソースの場合、そのままでは、

static let background: ImageResource

のような定義になりますが、SomeFeature/some_feature_background.imagesetのようにprefixをつける形でリネームすることで、

static let someFeatureBackground: ImageResource

のようなコードが生成されるようになります。

また、すでにprefixがついているアセットも多く存在したため、SomeFeature/some_feature_some_feature_background.imagesetのように二重にprefixがつかないように考慮する必要がありました。
そのような注意点を踏まえて、

  • 各imagesetの名前を、現在のディレクトリ情報をprefixとして含むようにリネーム
    • ただし、すでに含まれているものはスキップ
  • コード内の参照を、ImageResource経由になるように置き換え
  • IB内の参照を、最初のリネームを反映するように置き換え

というフローで対応しました。
先ほどの文言のString Catalogsへの移植よりは、リネームを挟む点や、IB内の置き換えも含むことから難易度が高くなっています。

置き換えパターンの解説

上記のSomeFeature/background.imagesetの例を利用すると、some_feature_background.imagesetに修正するにあたり、

  • Asset.Images.SomeFeature.background.swiftUIImageImage(.someFeatureBackground)に置き換え
  • Asset.Images.SomeFeature.background.imageUIImage(resource: .someFeatureBackground)に置き換え
  • IB内のimage="background"image="some_feature_background"に置き換え
  • IB内の<image name="background" ...<image name="some_feature_background"に置き換え

などのパターンが必要になります。(実際にはselectedImageなど、IBのパターンはより多くあります。)

置き換えパターンが多くある上に、キャメルケースとスネークケースの2つに対応する必要があるため、複雑度はより高くなっています。

生成AIを利用したスクリプト生成とレビュー、修正

String Catalogsの時と違いルールが複雑だったため、複数のプロンプトに分割してルールを追加する方針で進めました。
それぞれのルールでイレギュラーパターンに対応する必要があり、1回のやり取りでは上手く動きませんでしたが、トータルで10回ほどのやり取りで完成系のスクリプトを得ることができました。

こちらのケースではGPT-5を利用しています。
主なプロンプトをまとめて日本語に直すと、以下のようになります。

画像アセットの命名違反を検出するPythonスクリプトを作成してください。
すべてのimagesetはその親ディレクトリの名前をprefixとして含む必要があります。
画像アセットは`Packages/.../Resources/Images.xcassets`に配置されています。
キャメルケースとスネークケースの差は許容されます。
不足しているprefixを各imagesetに付け加える機能を作成してください。
prefixはスネークケースで付け加える必要があります。
また、dry runモードも追加してください。
修正機能にSwiftコードの参照を置き換える機能をつけてください。
コード内ではアセットはキャメルケースで扱われます。
Swiftコードは`Packages/xxx/Sources`にあります。
`Asset.Images.SomeFeature.background.swiftUIImage`は`Image(.someFeatureBackground)`に、`Asset.Images.SomeFeature.background.image`は`UIImage(resource: .someFeatureBackground)`のようになります。
新しいルールを追加してください。
アセットの名前が変更された場合、対応するコードも修正される必要があります。
すなわち、`background`アセットが`some_feature_background`にリネームされた場合、`Image(.background)`は`Image(.someFeatureBackground)`のようになります。

最終的に650行ほどPythonスクリプトになり、処理のフローは想定通り以下のようになりました。

  • os.walk.imagesetを走査して、キャメルケースからスネークケースへの変換を挟みつつ、命名違反ファイルのパスと修正後の命名を含むリストを作成
  • Swiftファイルをos.walkで走査しつつ、上記のパターンに当てはまる修正前のアセット名を含むコードを、正規表現で検出して置き換える
  • 同様にIBファイルをos.walkで走査しつつ、上記のパターンに当てはまる修正前のアセット名を含む内容を、正規表現で検出して置き換える
  • 最後にshutil.move.imagesetのリネームを行う

一度でうまく動かなかった部分として、処理の順番の関係で修正漏れや、関係のない部分の置き換えが発生するといった問題はありました。
また、より単純な問題として、Pythonの実行時エラーが生じる場合もありました。
これらは、追加のプロンプトでフィードバックしたり、Pythonのコードを直接修正することで解決しました。

スクリプトの実行結果と評価

結果的に2時間ほどでスクリプトが完成して、XLIDのように略語で大文字が連続するケースの変換ブレを除き問題なく修正することができました。
全体で200ケースほどありましたが、数行の微調整を除き問題なく動作してリリースすることが出来ています。

シェルスクリプトでやるには複雑すぎるのと、普段使わないスクリプト言語で調べながら実装すると時間がかかりそうという感触を持っていたため、手作業と比べて大きく効率化できたかと思います。
また、プロンプトでどこまで記載するかは難しいですが、今回は文字列置き換えやosモジュールの利用など個々の処理自体はありふれたものなので、詳細な設計をする必要はなさそうでした。

おわりに

今回のSwiftGenからの移行対応では、Pythonスクリプトを使い捨てで利用するという条件と、100%の精度が求められない条件があったため、効率の面で生成AIの利点を大きく活かすことができました。
一方で、このケースでは問題にならないものの、生成されたスクリプトをレビューした際に、冗長な表現や命名の問題など保守性の面で課題がありそうという印象を受けました。

東急線アプリのiOSチームは現在一人で開発を進めているため、実装の他にもクラス設計の壁打ちやコードレビューやに活用しています。
使っているプロダクトやモデルの違いはありますが、クラス設計ではかなり有用に感じつつ、コードレビューについてはほぼノイズになってしまっています。
現時点ではAIの進歩を待ちつつ、生産性の向上につながるか否かををしっかり見極めてから、開発に取り入れていこうと考えています。

https://apps.apple.com/jp/app/東急線アプリ-東急電鉄-東急バス公式の時刻表-運行情報/id604757991

https://apps.apple.com/jp/app/東急カードプラス/id684161442

東急URBAN HACKS

Discussion