ライブラリ管理をCarthageからSwiftPackageManagerに移行する


この記事はand factory.inc Advent Calendar 2022 6日目の記事です。
昨日は　@nabetaro_jp さんの [[Flutter] あの赤いエラー画面をカスタマイズする](https://qiita.com/nabetaro_jp/items/1648cfc672cb702f8c23 "") でした。

# 概要

先日、自分が参画しているプロダクトでライブラリ管理ツールをCarthageからSwift Package Manager（以下、SPM）に移行するという機会に遭遇したので、その過程で行ったことをまとめます。

# 背景

該当のiOSアプリのプロジェクトではライブラリ管理にCarthageとCocoapodsを併用しており、Carthageはビルド時間の短縮に大きく寄与していたものの、ライブラリによってはビルドや依存解消に失敗するケースが頻発していた（特にXcode12以降）こと、CI/CDの連携でエラーが起こる、XCFrameworksのサポートが怪しいなどの課題がこれまで存在していました。

そこでApple公式のパッケージマネージャーであるSPMがiOS開発者の間で広まってきたこともあり、今回Carthageで管理していたライブラリを思い切ってSPMとCocoapodsに移行する運びになりました。

# やったこと
## project.yml、Podfileの更新
弊アプリでは.xcodeprojファイルの生成にXcodegenを使用しているため、まずは`project.yml`にSPMに移行するライブラリを追記していきます。
```yaml:project.yml
...
packages:
  # コミット指定の場合
  FSPagerView:
    url: https://github.com/WenchaoD/FSPagerView.git 
    revision: a8c14cfb2c07b91ccf6a19dfe126b86103f56f74
  # バージョン指定の場合
  Reusable:
    url: https://github.com/AliSoftware/Reusable.git
    from: 4.1.2
  # ブランチ指定の場合
  Lottie:
    url: https://github.com/airbnb/lottie-ios.git
    branch: master
  ...
```
Xcodegenを使わずXcode上で設定したい場合は、Xcodeの`File` > `Add packages`などから簡単に追加できます。

https://qiita.com/hironytic/items/09a4c16857b409c17d2c

ライブラリの移行先をSPMにするかCocoapodsにするかは、当該ライブラリのリポジトリにパッケージ構成を定義する`Package.swift`があるかどうか、（SPM対応しているように見えても導入できないケースがあるので）実際に導入して正しく依存解消＆ビルドできるか否かで判断しました。

Cocoapodsに移行する場合は`Podfile`を更新します（説明は割愛）

## Carthage関連のリソース削除
これでSPMからライブラリをインポートすることができるようになりました🎉

それに続き、不要になったCarthage関連のファイルを削除します。
整理対象はCartfile、Cartfile.resolvedと、後はproject.ymlのCarthage用のコードです（環境に合わせて要対応）

## CIのビルドキャッシュ管理の設定
ここまでやれば特に問題なくビルドできるようになったのですが、我々のプロジェクトで使用しているCI上でビルドする際にキャッシュ管理をするためには別の設定が必要でした。

https://medium.com/uptech-team/swift-package-manager-and-how-to-cache-it-with-ci-14968cd58c5f

例としてCircleCIでSPMのキャッシュ管理するためには`.circleci/config.yml`を下記のように更新します。

```yaml:config.yml
  ...
  run_fastlane:
    description: "Deploy fastlane"
    steps:
      - restore_cache: # SPM関連のキャッシュ対応のため
          keys: 
            - v1-spm-cache-{{ checksum "ourproject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" }}
            - v1-spm-cache-
      - run: 
          name: Run fastlane
          command: bundle exec fastlane "$CIRCLE_BRANCH"
          no_output_timeout: 1h
      - save_cache:
          key: v1-spm-cache-{{ checksum "ourproject.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" }}
          paths:
            - SourcePackages
  ...
```
やってることとしては、キャッシュを保存するパスを指定（ここでは`SourcePackages`）し、キーからこれを保存＆呼び出しできるようにするという設定になります。

同様にCircleCI上でのビルドに使用するfastlaneの`Fastfile`にも、SPMのキャッシュファイルのパスを指定するオプションである`cloned_source_packages_path`を追記します。
```yaml:Fastfile
desc "build develop"
    lane :develop do
        fix_version(build_configuration: "Debug")
        gym(
            scheme: SCHEME,
            configuration: "Debug",
            skip_package_ipa: true,
            skip_archive: true,
            skip_codesigning: true,
            cloned_source_packages_path: "SourcePackages"  # SPMキャッシュ用ディレクトリ
        )
    end
```

# ビルド時間の壁を越えたい

これでCI上でも一度ビルドしたパッケージはキャッシュすることでビルド時間をある程度短縮できるようになりました。
しかし、それでもCarthage時代より30%ほどビルド時間が長い、って点で引っ掛かかりがありました。

原因を探ってみると、どうやらデータベース管理のためのライブラリである`Realm`がビルド時間の足を引っ張っていたことが判明。

そこで`Realm`のみSPMではなくXCFrameworkでプロジェクトに導入する流れになりました。

# XCFrameworkでRealmを入れる

XCFrameworkとは、ビルド済みのSwiftパッケージを配布するためのフォーマットのこと。

これまでCarthageを使っていた時はあらかじめライブラリをframework化してキャッシュすることでビルド時間を短縮していたのですが、残念なことにSPMではライブラリのソースコードはキャッシュしてくれるが、これをビルドしたバイナリはCIでキャッシュできない（キャッシュしても機能しない）という事実が調べていて分かりました。

https://qiita.com/temoki/items/045c375e1127dbd1bbf2

では実際の導入ですが、Realmは公式リポジトリで最新版のxcframeworkを配布してくれているので、やるこことしてはこれを落としてプロジェクトにリンクするだけです。簡単ですね。

https://github.com/realm/realm-cocoa/releases/

上記ページから欲しいバージョンの`Carthage.xcframework.zip`をダウンロードします。

ダウンロードが完了したらこれを解凍し、xcframeworks管理用の任意のディレクトリに移し、これをプロジェクトにリンクするための設定を`project.yml`に追記します。

```yaml:project.yml
    ...
    dependencies: 
      - framework: xcframeworks/Realm/Realm.xcframework 　     # 追加
      - framework: xcframeworks/Realm/RealmSwift.xcframework  # 追加
      - package: APIKit
      - package: CryptoSwift
    ...
```
実際のプロジェクトではこの辺りの処理を自動化するためのスクリプトを下記記事参考に書いています。

https://zenn.dev/niccari/articles/f34dc7090e52b5

ここまでやって、漸くSPM+Cocoapods+XCframeworksの編成でビルド時間をほぼ以前の水準まで戻すことに成功しました🎉

## 補足（XCFrameworks導入後に実機ビルドで失敗した件）

XCFrameworkの導入作業の途中、シミュレーターやCIではビルドできるのに実機ではビルドに失敗するという事象に見舞われました。

色々原因を調べた結果、project.yml上でRealmに依存するTarget（下記で`TargetB`）だけにdependenciesとframeworkの設定をしていたことが原因らしく、このTargetに依存する別のTarget（下記で`TargetA`）にも同じ設定をしてあげる必要がありました。

```project.yml
targets:
  TargetA:
    ...
    dependencies:
      - target: TargetB
      ...
      - framework: xcframeworks/Realm/Realm.xcframework  # TargetAが直接依存していなくても設定しないと実機ビルドで失敗
      - framework: xcframeworks/Realm/RealmSwift.xcframework  # （同様）
     ...
  TargetB:
    ...
    dependencies: 
      - framework: xcframeworks/Realm/Realm.xcframework
      - framework: xcframeworks/Realm/RealmSwift.xcframework
      ...
```
これはXCFrameworkがそういう仕様なのか、Xcodegenがそういう仕様なのかよく分かっていないので、今後の要調査課題として残っております。何かご存知の方がいらっしゃれば教えてください。

## まとめ

CarthageからSPMへの移行が一通り終わってみての感想ですが、重い腰を上げるのが中々大変でしたが、いざやってみるとすぐに終わったという感じです。
ただビルド時間の側面ではまだまだ改善ポイントがありそうなので、引き続き色々模索していこうと思います。

明日のAdvent Calendarもお楽しみにしてください！