エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

iOSのプロジェクト管理をXcodeGenからSwift Package Managerに移行する

【マルチデバイスチーム ブログリレー5日目】

こんにちは、エムスリーエンジニアリンググループ マルチデバイスチームの渡辺です。 スマホアプリの開発(iOS/Androidネイティブがメイン、たまにFlutter)を担当しています。

マルチデバイスチームの開発するm3.comアプリ(iOS)ではプロジェクトとマルチモジュールの管理にXcodeGenを利用しているのですが、後述する課題を感じていました。そんな中、昨年@d_dateさんによりSwift Package Managerを利用したプロジェクト管理する記事が公開されました。

www.notion.so

この記事を参考に、1日目の記事でも触れていますが現在 脱XcodeGen を進めています。

プロジェクトごとに構成が違うため、必要なステップは変わってくるのですが、m3.comアプリの構成における進め方を紹介します。

m3.com アプリ

XcodeGenの課題感

  • モジュールを追加するたびにproject.ymlにボイラープレートをコピー&ペーストしなければいけない
  • ファイル構成の違うgitのブランチに切り替えるたびにプロジェクトファイルを再生成する必要があり、IDEの再読み込みが始まる
    • 大規模なプロジェクトだと読み込み時間が長い
    • 私はXcodeとAppCodeを併用しており、AppCodeは再読み込み中はファイル検索やコードジャンプなどが効かなくなるため、数分間作業が止まってしまう
  • .xcodeproj で管理しているせいか、モジュールがライブラリを利用すると原因不明のビルドエラーが発生することがある
    • XcodeGenの設定ミスなのかもしれないが、Xcode上で No such module 〜 といった詳細不明なエラーしか表示されないことが多い

元々のm3.comアプリの構成

  • プロジェクト管理
    • XcodeGen
  • ライブラリ管理
    • Swift Package Manager
    • CocoaPods
  • モジュール構成
    • アプリ本体
    • ドメイン
    • インフラ
    • 各サービス(UI)

リファクタリングの最初でXcodeGenを導入し、マルチモジュール化したのですが、まだアプリ本体に古いコードが大量に残っているためリファクタリングしつつモジュールに移動を進めています。

XcodeGen -> Swift Package Manager 移行のためにやった(やっている)こと

1. CocoaPods管理のライブラリをSwift Package Manager管理へ移行

CocoaPodsで管理しているライブラリをSwift Package Managerに移行しました。 ライブラリ側でPackage.swiftが提供されていれば基本的に問題なく移行できるかと思います。

m3.comアプリが利用しているライブラリの中にはCocoaPodsのみ対応しているものもいくつかありました。 その場合はGitHub上でforkし、自前でSwift Package Managerの対応しました。

Swiftのみで実装されたライブラリはPackage.swiftを用意し、パスを整理すればほとんどのケースで問題ありませんでした。

Objective-Cのみで実装されたものは、ライブラリごとに必要な対応が異なり、説明が難しいため割愛します……

Objective-C製のライブラリをSwift化+Swift Package Managerに対応する一例です。 github.com

2. CocoaPodsの利用を廃止

  1. Podfile , Podfile.lock , Pods/ を削除
  2. .xcworkspaceをgit管理から外している場合、再度git管理に戻す
  3. project.ymlpod installがある場合、記述を削除
  4. XcodeGenでプロジェクトを生成
  5. CocoaPodsのインストールスクリプト(Gemfileなどを含む)があれば削除

これでCocoaPodsに関するものがなくなりました。

3. Package.swiftを生成

プロジェクトルートに任意のディレクトリ(以後AppFeatureとします)を作成し、その中に移動します。

$ swift package init

上記のコマンドを実行するといくつかのファイルやディレクトリが生成されます。

(主要なものを抜粋)

  • Package.swift
    • モジュールや外部ライブラリの参照を定義するファイル
  • Sources/{ModuleName}/
    • 各モジュールのソースコードを配置するディレクトリ
  • Tests/{TestModuleName}/
    • 各モジュールのテストコードを配置するディレクトリ

4. Packageをプロジェクトに追加

{ProjectName}.xcworkspace/contents.xcworkspacedata をエディタで開き、以下を<Workspace>内に追記します。

<FileRef
   location = "group:AppFeature">
</FileRef>

Xcodeのプロジェクトを一度閉じて再度開くと図のようにPackageが追加されています。

Package追加後

次にproject.ymlを開き、AppFeatureをライブラリとして追加します。

# 最低限必要な箇所を抜粋
packages:
  AppFeature:
    path: AppFeature

targets:
  {AppName}:
    dependencies:
    - package: AppFeature

この段階でXcodeGenでプロジェクトを生成すると、デフォルトで用意されたSources/AppFeatureがアプリからはAppFeatureモジュールとして見えるようになり、import AppFeatureと記述してビルドが通るようになっています。

5. XcodeGen管理のモジュールをSwift Package Manager管理下に移動

XcodeGenで管理していたモジュールの中で、まず他のモジュールを参照していないドメインモジュールをSwift Package Manager管理下に移行しました。

Package.swiftを開き以下のように修正します。

// swift-tools-version: 5.7
import PackageDescription

let package = Package(
    name: "AppFeature",
    platforms: [
        .iOS(.v13) // プロジェクトに合わせて変更
    ],
    products: [
        // name: ライブラリ名
        // targetsに指定した文字列: 公開するモジュール名(モジュールごとに追加)
        .library(name: "AppFeature", targets: [
            "Domain"
        ])
    ],
    dependencies: [
        // モジュールが利用するライブラリの定義
        .package(url: "https://github.com/apple/swift-algorithms", exact: "1.0.0")
    ],
    targets: [
        // target: モジュールの定義
        .target(
            name: "Domain",
            dependencies: [
                // モジュールが利用するライブラリを追加
                .product(name: "Algorithms", package: "swift-algorithms")
            ]
        ),
        // ユニットテスト用のモジュール定義
        .testTarget(
            name: "DomainTests",
            dependencies: [
                "Domain"
            ]
        )
    ]
)

次にSources/Domain/ に元のドメインモジュールのコード、Tests/DomainTests/に元のドメインモジュールのテストコードを移動します。 Xcodeが自動的にPackageを再読み込みするので、ビルドが通りテストが成功すれば完了です。
(経験上一発でビルドが通ることのほうが少なく、実際にはエラーメッセージを元にひたすら不足しているimport文を追加してまわる作業があったりします)

現時点では上記のドメインモジュールに加えてインフラモジュールの移行まで完了しました。

現状感じているメリット・デメリット

まだ移行中ではありますが、個人的に感じているメリットを挙げます。

  • Package内のファイルの追加や移動は、XcodeGenでプロジェクトの再生成が不要になった
  • CIのユニットテスト実行時間が約半分になった
    • これまでアプリ本体+各モジュールごとにユニットテストを実行していた
    • Swift Package Manager管理下の全モジュールを一度のユニットテストで実行できるようになり、さらにビルド時間も大幅に減った
  • CLion+Swiftプラグインで開発できるようになった
    • 個人的にJetbrains IDEを愛用しており、AppCodeのサポートが終了してしまい途方に暮れていたが、自分の用途ではCLionが代替として十分使えることがわかった
    • とはいえまだSwift Package Manager管理外のファイルが多いのでAppCodeの利用がメイン

デメリットとしてはXcode 14環境でCIが不安定な点があります。 ローカルでは問題ないのですが、CIで実行するとたびたび以下のエラーが発生します。

xcodebuild: error: Failed to build workspace AppFeature with scheme AppFeature.
Reason: internal error: could not generate PIF because the workspace has not finished loading or is still waiting for package resolution

GitHubにIssueは上がっているため、解消されることを期待しています。 github.com

今後の予定

以下を予定しています。

  1. 各サービスのモジュールをSwift Package Manager管理下に移行
  2. アプリ本体のコードをリファクタリングしつつSwift Package Manager管理下に移行
  3. アプリ本体が必要最低限になった段階でXcodeGenの利用を廃止

We are hiring!

マルチデバイスチームではiOSだけでなくAndroid、そしてFlutterのアプリ開発も行っており、プログラミングが好きなエンジニアを募集しています。 興味がありましたらぜひご応募ください!

jobs.m3.com