🤐

「機能開発を進める」「SwiftPMへ移行する」「両方」やらなくっちゃあならないってのが

2022/12/19に公開

「開発者」のつらいところだな。

※この記事は Luup Developers Advent Calendar 2022 の19日目の記事です。

久しぶりに記事を書きます。こんにちは。tarunonです。Luupではアドバイザーとしてお世話になっています。日々の開発方針の壁打ちや、パフォーマンスチューニングなど、お手伝いさせていただいています。
その中で、今年の1〜3月に、LUUPアプリのパッケージマネージャーをCocoaPodsからSwiftPMへの置き換えを行いました。この記事は当時のまとめとなります。

SwiftPMが使いたい!

SwiftPMが発表されてしばらくが経ち、最初は無かった開発に必要な機能(Bundle追加、pre/post build script)も揃ってきましたので、そろそろSwiftPMを依存解決ツールとして取り入れる機運が高まってきてるのではないのかと思います。
SwiftPMを利用していないプロダクトにおいては、CocoaPodsもしくはCarthageを利用していると思います。これらのツールは、mainブランチの更新は頻繁に行われていますが、リリースの頻度は確実に下がっています。
オフィシャルな依存解決ツールが出てしまった以上、仕方ないことです。
近々でDeprecatedになっていないとはいえ、長い目で見てSwiftPMに乗り換えていくのは理に叶っていると考えています。

SwiftPMは依存解決ツールとしての役割と、もう1つ、プロジェクト構成の記述もできます。今までだとxcodeprojを用意してフレームワークを宣言していたものが、Swiftでの記述に切り替わる。ファイルツリーとxcodeprojのシンクなんかも不要になります。同様の問題の解決を目指したプロダクトとして、XcodeGenが近いでしょう。ということで、これも置き換えるか、あるいはXcodeGenの適用範囲を狭めることができます。

SwiftPMを導入することで、ざっくり今までCocoaPods+XcodeGenで構築していた技術スタックを置き換えることができます。
今すぐにでも手をつけたいところですが、我々は「開発者」です。「機能開発を進める」ことも両立しなければいけない、両立するべきです。

Luupでの事例

LUUPアプリでは依存解決ツールにCocoaPodsを利用していました。フレームワークの分割はしておらず、したがってXcodeGenは存在せず、1つのxcodeprojで動いていました。
これをSwiftPM中心のプロジェクトに「なるべく差分が少ない状態で」書き換えていきます。差分を少なくする目的は、他ブランチでの機能開発と並走するためです。
目標は @d_date が提唱しているプロジェクト構成です。
https://www.notion.so/Swift-PM-Build-Configuration-4f14ceac795a4338a5a44748adfeaa40
ここからは、実際の作業で意識したことを書いていきます。

古いxcodeprojはそのままにしておく

今までのxcodeprojから完全に分離して、SwiftPMを導入したxcodeprojは完全新規に作ります。SwiftPM導入の作業をしている間も、他の機能は追加され、今までのxcodeprojはどんどん変化していきます。削除してしまっては、都度コンフリクトが発生してしまいます。
xcworkspaceからの参照さえ切ってしまえば無効化できるので、そうしておきます。削除するのは完全に統合が完了した後でOKです。
新規のxcodeprojは適当なディレクトリーを新規に切って、その中で管理するのが良いでしょう。
大事な点として、既存のxcodeprojで宣言されていたPlatformのライブラリーへの依存は、1つも漏らすことなく全て行わなければいけない、ということです。
サードパーティのライブラリーの中には、Platformのライブラリーへの依存をweak参照で行ってるものがあり、「ビルドは通るし一見アプリも動いているが、効果測定が転けている」というQAをすり抜ける不具合を産むことになってしまいます。しまいました。
関係者の方々、ご迷惑をおかけして申し訳ありませんでした。

既存のディレクトリー構成は変更しない

LUUPアプリのような構成であれば、ルートディレクトリー以下プロダクト名のディレクトリーに、すべてのソースコードが含まれている状況かと思います。そして、そのディレクトリー構成は、SwiftPMの移行では変更しません。
そのディレクトリーは機能開発によって最もアグレッシブに変更されているため、SwiftPM導入の作業で変更をしてしまうと巨大なコンフリクトが発生してしまいます。期間が長引けば、頓挫の可能性も高くなってきます。
代わりに、以下のようにSwiftPMでtargetの参照するpathを明示できるので、それを使いましょう。

.target(
    name: "App",
    dependencies: [
    ...,
    ],
    path: "LUUP", // <- ココ
    swiftSettings: defaultSwiftSettings
),

InterfaceBuilderの参照先を切り替える

元々、アプリ本体にBundleされていたInterfaceBuilder群を切り出したため、customModuleがアプリ本体のバンドル名で指定されていたものを、切り出し先のSwiftPMパッケージ名の参照に置き換える必要が出てきます。
大量のInterfaceBuilderをいちいち書き換えたり、差分を取り込むたびに修正していては単純作業とはいえ多大な労力を払うことになるため、簡易なスクリプトを用意しました。

find "${PROJECT_DIR}/../LUUP" -name "*.storyboard" -print0 | xargs -0 sed -i "" 's/customModule="LUUP" customModuleProvider="target"/customModule="App"/g'
find "${PROJECT_DIR}/../LUUP" -name "*.xib" -print0 | xargs -0 sed -i "" 's/customModule="LUUP" customModuleProvider="target"/customModule="App"/g'

storyboardとxibファイルを探索し、アプリ本体のバンドルを指定しているものを、Appライブラリーを指定する様に差し替えています。
機能開発によって取り込まれた新しいファイルや、変更があった場合もスクリプトを走らせれば対応完了、という算段です。

ビルドフラグを使っているところを整理する

ここはあまり上手くできなかったところでした。他にもっといい方法があると思います。
次の様なコードは、あらゆるプロジェクトに存在していると思います。

#if DEBUG
//デバッグ処理が書かれている
#else
//本番用の処理が書かれている
#endif

SwiftPM以下に配置したコードでは、ビルドフラグを直接刺すことができなかったため、新しい方法に置き換えなければならなくなりました。
LUUPアプリではFirebaseを利用しており、そのConfigファイルをmainバンドルから読み込んでいたので、その設定を焼き込んだstruct Configを利用することにしました。ビルド済みのバイナリーにデバッグコードが含まれるのは気持ち悪いですが、他に優先することが多くあったため、ここは調査もほどほどに、一旦諦めることにしました。

if Config.buildEnviroment == .develop {
//デバッグ処理が書かれている
} else {
//本番用の処理が書かれている
}

SwiftPM対応してないライブラリー群をなんとかする

なんとかしなければいけません。LUUPアプリではGoogleMapsSDKとSVProgressHUDがそれでした。
Carthageに対応している「ソースコードが含まれる」ライブラリーは、Carthageでxcframeworkを生成し、その成果物をSwiftPMのバイナリーとして配信することで解決ができます。問題はGoogleMapsSDKです。
幸い、SwiftPM化をチャレンジされてるリポジトリーがあったので、こちらを参考にして進めました。
https://github.com/YAtechnologies/GoogleMaps-SP
これに関してはバージョンごとに様々な苦労があったのですが、解明するに至るところまで調査が出来ていないため、詳細についてはここでは割愛させてください。

一旦、これらのライブラリー群は別リポジトリーにまとめて、xcframeworkとしてgithub release上で配信することにしました。

CIを切り替える

CocoaPods準拠で動いていたCIを、SwiftPM準拠に置き換える必要があります。ちょうど @k_katsumi がGitHub Actionsで結果をみやすくするツールxcresulttoolを出された時期で、元々Bitriseで動いていたCIを、GitHub Actionsに移してみましょうという動きがありました。古いCIはそのままに新しいCIをGitHub Actionsへ置くことにしました。オーソドックスですがMakefileを作ってそれを叩くだけ、手元のテストとCIの動きを揃える、ということをやりました。
なおGitHub ActionsでのCIは、現在はXcodeCloudへの移行が行われました。詳細については大瀧さんの記事をご覧ください。
https://zenn.dev/luup/articles/ios-ohtaki-20221213

import UIKitを明示する

今までXcodeで暗黙的にimportされていましたが、なくなったので明示的にimportしました。
こちらは特にツールも用意せず、コンパイルエラーが出るたびに愚直に対応しました。

Bundleリソースを取り込み直す

ここが一番大変な部分でした。
Bundleリソースの管理に関して、R.swiftを利用していたのですが、R.swiftがアプリ本体以外との連携が弱く、xcodeprojにリソース群を配置せざるを得ない状況となっていたことがあります。
その結果、新しいxcodeprojにBundleリソース群を取り入れ続けることになりました。xcassetsファイルは共通のものを使うようにしたため、そこまで大きな作業は発生しませんでしたが、stringsファイルなどは追加があったため、差分を取り込むたびに都度追記していくこととなりました。

結果とまとめ

他の開発に影響を与えずにやるぞ!という高い志を持って挑んだわけですが、結果としてどうしようもない項目があり、多くの差分を産むこととなってしまいました。
2022/01/19に作業を開始して、マージしたのは2022/03/03でした。
ある程度developブランチが進捗したら、都度developブランチからの取り込みを行い、スクリプト群を走らせてビルドが通る状態にする。
それを繰り返しながら、developブランチに統合できるタイミングを測っていました。
最終的なDiffは+5374 -2483でした😇
これを統合した直後の、機能開発のブランチの作業はどうだったのでしょうか。
@MasamiYamate fix conflict 1fa368e +5382 -2483
@HikaruOhtaki solve conflict 639ad79 +1153 -833
そこそこ大きめのコンフリクト解消コミットがあり、それなりの負担を発生させてしまった、という結果でした。
また不具合としては、staging環境で画像の参照がおかしいというのが1つ、リリース後に効果測定が一部うまくいかないと言うのが1つ(本当にすみません)、発生してしまいました。
全部うまくやる魔法みたいな方法はなく、地道な努力が実を結ぶってことですね。
万事うまく行った、とはいきませんでしたが、SwiftPMへ移行する、と言う当初の目的は達成できました。
4月以降、LUUPアプリではモジュール分割や、それに伴うコードの整理などを機能開発と並行して行っています。
それらの作業において、SwiftPMに移行したことが大いに役に立っています。
適切なタイミングで手を打てたのではないのでしょうか。

まだSwiftPMを導入していないチームで、この記事を役立てていただければ幸いです。
Luupでは機能開発とリファクタリングのバランスをとりながら、良い塩梅で開発を進めていると感じています。ご興味をお持ちいただけたら、ぜひお声がけください。

https://recruit.luup.sc

Luup Developers Blog

Discussion