こんにちは!コネヒトでiOSエンジニアをやっていますyanamuraです。
ママリのiOSアプリでモジュール分割を行いましたのでその内容について記載しました。
なぜモジュール分割
目的としては大きく2つありました。
1つ目はApp Extensionsをつくるのを楽にしたいためです。
以前ママリでApp Extensionsを使ったプロトタイプで作ろうとした際、App Extensionsからママリアプリの一部のコードを再利用しようとしたところ、依存関係の問題で芋づる式にたくさんのコードを取り込まなければならなくなりとても大変な思いをしました。App Extensionsで共通利用しそうな部分はモジュール化して分割しておくべきだったなという経験からモジュール分割しようという思いに至りました。
2つ目はSwiftUIのプレビューを速くしたいためです。
ママリのiOSアプリではSwiftUIを導入していましたが、Previewがめちゃめちゃ遅いというか場合によってはタイムアウトしてしまい使いものにならないといった問題がありました。モジュール分割することでこの問題を少しでも解消したいという狙いが当初ありました。しかし、MacをApple siliconのものにリプレースして、Rosetta2ではなくApple silicon上でXcodeが動くようにこちらで対応した結果劇的に改善されPreview問題は解決しましたので、こちらの目的は薄くなりました。
モジュール分割の方針
ママリのiOSアプリの構成は、CocoaPodsを使ってパッケージを管理しています。
SwiftPackageManagerを使ってマルチモジュールやパッケージを管理したいところですが、ステップを刻んで少しずつ進めていこうと思い、まず第一歩としてパッケージ管理はCocoaPodsのままで、モジュール分割はEmbedded Frameworkを用いる構成にしました。
モジュールの分け方
モジュールの分け方は、縦(機能ごと)に分けたり横(レイヤーごと)に分けるなどといろいろなやり方がありますが、ママリではアーキテクチャはMVVMになっておりView,ViewModelとModelで2分割しました。
この分け方であれば、目的が満たせる最低限の分割で、最初にはじめるにはよいのではと考えたためです。
モジュール分割する際に起こった問題
いきなりEmbedded Framework化すると変更量が半端ない
ママリの場合1万行くらいの規模をEmbedded Frameworkに切り出すことになったのですが、ビルドが通るようになるまでにかなりの修正が必要で、一旦はじめてしまうと終わるまで結構な時間を要することになります。モジュール分割のために普段の開発を完全に止めるわけではなく、その間もコードの編集が行われるため、同期が大変になります。
これを避けるために、一旦プロジェクト内のディレクトリ構成だけ分割し、Embedde Framework化する前に修正が必要なところ(依存関係の修正やpublic化など)を修正しました。これによりEmbedded Frameworkに実際に分割した際に必要な変更を減らし、スムーズにEmbedded Framework化できました。
structのpublic化
Swiftのstructにはmemberwise initializer(https://github.com/apple/swift-evolution/blob/main/proposals/0018-flexible-memberwise-initialization.md) というstructのpropertiesを初期化するinitializerが自動で生成されます。しかしこのinitializerの可視性はinternalとなっているのでstructをpublicにするとmemberwise initializerを使っていた場合はpublicなinitializerを手動で追加しなければなりませんでした。
手動の追加ですが、Xcodeの機能でstruct名を右クリックしてRefactor->Generate Memberwise Initializer使うとコードを生成することができます。
static framework/libraryのimport問題
アプリを動かすと以下のようなメッセージがログに表示され、simulatorで動かすとクラッシュするといった問題が発生しました。
Class PodsDummy_XXX is implemented in both /xxxBuild/Products/Debug-iphonesimulator/YYY.framework/YYY (0xfffffffff) and xxx/yyy.app/zzz). One of the two will be used. Which one is undefined.
これはアプリとモジュール分割したframework(dynamic)で同じstatic libraryに依存していた場合に発生しました。 Embedded Frameworkを作成するとデフォルトではdynamic libraryに設定されています。アプリとdynamic frameworkの両方で同じstatic libraryに依存するとシンボルの重複が発生してしまいます。
手っ取り早い解決方法がモジュール分割したframeworkをdynamic frameworkではなくstatic frameworkに変えてやることです。Build SettingsのMach-O TypeをStatic Libraryに変更することでstatic framework化することができます。ママリでは現状static frameworkにするデメリットはなさそうでしたのでこちらのやり方で対応しました。
もう一つの方法は、アプリ側のOTHER_LDFLAGSから重複するstatic libraryを消してリンクしないようにする方法です。 CocoaPodsの場合はPodsfileで頑張る必要がありそうでした。https://github.com/CocoaPods/CocoaPods/issues/7126
モジュール分割してよかったこと
よかったことは、責務があやふやなところが炙り出されたことです。 これまでもフォルダ構成で責務は別れていたように見えましたが、いざモジュール分割のためにファイルを振り分けてみると、これは本来こっちじゃないよねといったものや、Model側からのViewへの依存といった問題点などが浮き彫りになり、よいリファクタリングの気づきになりました。
また、差分ビルドの時間を時間を比較してみたところ、10秒くらい速くなりました。(一つメソッドを追加して計測)
差分ビルド時間(sec) | |
---|---|
before | 40 |
after | 30 |
まとめ
今回モジュール分割をすることにより、目的は達成することができました。今後はSwiftPackageManagerを使った構成に移行していこうと思っています。
コネヒトでの開発に興味を持っていただいた方はカジュアルにお話しましょう〜 TwitterのDMなどでも大丈夫ですのでお気軽にどうぞ 日本中の家族をITの力で笑顔にしたい、iOSエンジニア募集! - コネヒト株式会社のモバイルエンジニアの採用 - Wantedly