Kyash iOSアプリのライブラリ管理をSwift Package Managerへ完全移行しました

KyashでiOSエンジニアをしている@nekowenです。
今回はiOSチーム内で以前から取り組んでいたライブラリ管理のSwiftPMへの完全移行を果たしたため、その経緯と結果について公開します。

なぜSwiftPMに移行したのか

KyashではSwiftPM導入以前はCarthageとCocoaPodsを併用していましたが、どちらもツールの導入が必要で、事前にコマンドを叩いてライブラリをFetch、あるいはビルドする必要がありました。

またCarthageなどはビルド時間の観点から事前ビルドしたFrameworkをgitで管理していたこともあり、ライブラリの更新時はPRが毎回大量のdiffで埋まっている状態で、レビューがしづらいといった課題もありました。

そんな中、Xcode 11からSwiftPMが直接扱えるようになりました。外部ツールの導入が一切不要になるため導入・管理コストを減らすことができ、Xcodeを開くだけで自動でFetchしてくれることに感動し、これは移行の良い機会だと考え完全移行を計画しました。

完全移行までの流れ

このような経緯からKyashでは去年より段階的にSwiftPMへ移行を進めていましたが、ライブラリによってはアップデートが必要で、アプリ側のマイグレーション対応が必須となり、すぐに移行できないケースがちらほらありました。 そのため移行できていないライブラリをCocoaPodsに残しておき、基本的にはSwiftPMを利用する併用パターンで開発を回していました。

この構成でしばらく開発が回せていたこともあり、移行そのものが止まっていた時期もありましたが、開発の空き時間にライブラリのマイグレーション対応を進めることで徐々にCocoaPodsからライブラリを剥がせる状態になりました。

そして今年の3月、ようやく全てのライブラリをSwiftPMへ移行することができました🎉

最後の移行PRがマージされたとき

移行した結果

完全移行したことによって得られたメリットの一つとして、iOSリポジトリのセットアップコストが下がったことが挙げられます。

KyashではiOSエンジニア以外にもAndroidエンジニアや他部署ではQAチームがリポジトリに触る機会があり、その方たちにもセットアップをしてもらうことになります。

セットアップ工程の一つであるツールの導入が減り、iOSアプリを起動するという目的であればRuby自体の導入も不要なため、従来かかっていた時間を大幅に削減できました。

二つ目のメリットとしては、XcodeGen利用時においてCocoaPodsによる再構成が不要になったことです。

Kyash iOSアプリではXcodeのプロジェクトファイルをXcodeGenで管理しており、主にファイル構成やBuild Phasesに変更を加えた際にプロジェクトファイルを再生成します。

CocoaPodsを利用している場合、毎回このプロジェクトファイルに対して pod install を実行し再構成を行う必要がありますが、SwiftPMを利用していればこの作業が不要になります。 作業自体はXcodeGenを走らせた時に自動実行でき、install自体もそこまで時間のかかるものではありませんが、XcodeGen単体で利用できるようになった面は大きいと考えています。

SwiftPMへ移行して発生した課題

移行時に新たに発生した課題もありました。一つ目はプライベートリポジトリでライブラリを管理している場合、XcodeSSHキーを読まない問題がありました。

KyashではKotlin Multiplatform Mobile(以下、KMM)を採用しており、成果物であるFrameworkを専用のプライベートリポジトリ内で管理していたことがあります。 プライベートリポジトリにアクセスするにはSSHキーによる認証が必要なのですが、Xcodeがデフォルトのgit systemを利用しないケースがあり、Xcode上でGitHubアカウントの登録を求められることがありました。

できるだけXcodeでアカウント管理せず、既に登録しているSSHキーを利用してほしいと考えたため解決策を調べて以下のコマンドを呼び出すことでSSHキーが使われるようにしています。

defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES

現在ではKMMを扱うリポジトリをサブモジュールとして扱うようになっており、SwiftPM上でプライベートリポジトリを扱うケースがほぼなくなったためこの問題は起きづらくなっています。

二つ目は、Renovateによるライブラリの自動更新機能が使えなくなってしまった問題がありました。

Renovateはライブラリ管理ツールが管理するファイルを読み取り、ライブラリの更新をチェックし、新しいバージョンがあればプルリクエストの作成まで自動化してくれるサービスです。

CocoaPodsであればRenovateがサポートしているため特に手を入れることなく利用できますが、SwiftPMの場合XcodeGenを利用していてライブラリもYAML上で管理しているためそのままではRenovateで扱えない問題がありました。

調べてみるとRenovateには正規表現によってライブラリの更新検知を行うRegexManagerが存在しており、こちらの仕組みを扱うことでXcodeGenで管理しているライブラリの検知が可能になりました。

{
    ...
    "regexManagers": [
        {
            "fileMatch": ["^spm.yml$"],
            "matchStrings": [
                "github: (?<depName>.*?)\\s*version: (?<currentValue>[\\d.]*?)\\s+"
            ],
            "datasourceTemplate": "github-releases"
        }
  ]
}

またXcodeを開いた際に、SwiftPMが選択したライブラリのバージョンやその依存関係を含めた Package.resolved ファイルが書き込まれます。

このファイルが存在していればXcodeは書き込まれている情報を利用してライブラリのFetchを行うため、Renovateによる更新の際はこちらのファイルも書き換える必要があります。

対応方法として、RenovateのPRが作成された場合はGitHub Actionsを利用して Package.resolved を更新するスクリプトを追加することで自動で更新される仕組みにしました。

on:
  push:
    branches:
      - 'renovate/**'
    paths:
      - 'spm.yml'
jobs:
  update-package-resolved:
    runs-on: macos-12
    steps:
      ...
      - name: XcodeGen
        run: |
          mint run xcodegen --use-cache
      - name: Update Package.resolved
        run: |
          # resolvePackageDependencies を使うことで依存関係の更新だけ走らせることができます
          xcodebuild -resolvePackageDependencies -project Kyash-iOS.xcodeproj -scheme Kyash-iOS
      - name: Commit changes
      ...

最後に

今回はKyash iOSアプリでSwiftPMへ完全移行した流れと課題について説明しました。 Kyashでは利用しているライブラリの数が多く、完了までに時間がかかってしまいましたがそれでも移行するメリットは十分にあるなと感じました。

SwiftPMは定期的に機能追加が行われており、Swift 5.6でプラグイン機能がサポートされるなどより利用できる幅が広がってきているため今後も積極的に使っていきたいと考えています。

参考

RenovateによるiOSライブラリーの自動更新 - Speaker Deck

XcodeでSwift Package Manager実用段階 - クックパッド開発者ブログ