blog

DeNAのエンジニアが考えていることや、担当しているサービスについて情報発信しています

2022.03.16 技術記事

Swift Packageを中心とした構成に変えて、良かったこと・困ったこと

by qmihara

#ios #swift #skyleap

はじめに

日々 iOS アプリを開発されている皆さんこんにちは。 SkyLeap という Web ブラウザアプリを開発している qmihara です。

この記事では、 iOS アプリ開発で誰しも一度は経験したことがあるのではないかと思う project.pbxproj のコンフリクト地獄について、ついに我慢の限界に達し project.pbxproj によるプロジェクト管理から Swift Package を中心としたプロジェクト構成へ移行したことについてお話しします。

背景

SkyLeap の iOS アプリは現在 2 名で開発をしていますが、 2 名での開発においても project.pbxproj のコンフリクトは日常的に発生します。ファイルやフォルダの追加の他に、 Swift Package Manager を用いたライブラリ管理をしている場合、 Package.swift に記述される内容が project.pbxproj で管理されるため簡単にコンフリクトしてくれます。

そんな中、 iOSDC Japan 2021 で発表された Swift Package中心のプロジェクト構成とその実践 を見て、これなら!と思い導入することしました。

移行にあたり

移行前の SkyLeap のプロジェクト構成は次のようになっていました。

  • xcodeproj でアプリのモジュールと Embedded Framework を一つ管理
  • 開発用や本番用といった環境は Configurations で定義している
  • ライブラリ管理は Swift Package Manager で
    • CocoaPods や Carthage は卒業済み
  • その他に以下のツールを Build Phases の Run Script から利用している
    • SwiftGen
    • LicensePlist
    • Firebase Crashlytics へのシンボルアップロード
    • 自前のコードジェネレーター

移行後の構成は次のようにすると決めました。

  • Package.swift で既存のモジュール構成を定義
    • 元々マルチモジュール構成としていたためではないため細かく分割することは今回考えない
  • 環境別で xcodeproj を用意
  • xcworkspace で Swift Package と環境別 xcodeproj を管理
  • コードジェネレータ系は手動実行、あるいは環境別 xcodeproj の Run Script で実行
    • 手動実行の手間はあるものの、実行しなければコンパイルエラーとなり実行が必要であることには気付けるためこの時点では良しとしました

Package.swift で既存のモジュール構成を定義

既存のモジュール構成は、アプリのモジュールと内部で使用しているデータベースのエンティティの定義などをまとめた Core と呼ばれるモジュールの二つだけです。

一旦はこの二つをほぼそのまま Package.swift に移すのみとし、必要に応じて今後モジュールを分けて行けば良い、という方針としました。目的は project.pbxproj のコンフリクト地獄から脱出することですからね。

以下 Package.swift の抜粋です。

    products: [
        .library(
            name: "AppFeature",
            targets: ["AppFeature"]),
        .library(
            name: "Core",
            targets: ["Core"]),
    ],
    ...
    targets: [
        .target(
            name: "AppFeature",
            dependencies: [
                "Core",
            ],
            resources: [
                ...
            ]),
        .target(
            name: "Core",
            dependencies: [
                ...
            ]),
    ]

ただ Package.swift に定義をしただけでは動くわけではなく、全体的に次のような修正を行う必要がありました。

  • import しているモジュール名の置き換え
  • Storyboard や xib ファイル中のモジュール名の置き換え
  • UIImageUIColor など Bundle からデータを取得する箇所の修正
    • UIImage(named:) -> UIImage(named:in:) への置き換え

これらはビルドエラーを確認してスクリプトによる置換を行い、またビルドエラーを確認して、の繰り返しで対応しました。

コード中のモジュール変更と画像や色情報の取得方法変更

変更前
import SkyLeap
import SkyLeapCore

// 省略

UIColor(named: "skyLeapColor")
UIImage(named: "SkyLeapLogo")
変更後
import AppFeature
import Core

// 省略

UIColor(named: "skyLeapColor", in: .module, compatibleWith: nil)
UIImage(named: "SkyLeapLogo", in: .module, compatibleWith: nil)

Storyboard, xib のモジュール変更

Before After
Storyboard Storyboard Before Storyboard After

環境別で xcodeproj を用意

既存の Configurations で定義を分けていたやり方から、環境別で xcodeproj を用意する形に変更します。

Before After
Environment Settings Before Environment Settings After

環境別で xcodeproj を分けたことで次の利点がありました。

  • Firebase を使うにあたって必要となる GoogleService-Info.plist をシンプルに管理できるようになった
    • 以前は Build Phases の Run Script で環境別に用意した plist ファイルを GoogleService-Info.plist にコピーする、といったやり方をしていました
  • 環境別のアプリアイコンの定義が分けられた
    • これまでは Asset Catalog の中に全て入れてしまっていたのですが、 Asset Catalog 自体分けられるようになりシンプルになりました

xcworkspace で Swift Package と環境別 xcodeproj を管理

ここまでで作成した Swift Package と Xcode project を xcworkspace で管理します。

Before After
Xcode Project Navigator Before Xcode Project Navigator After

コードジェネレータ系は手動実行、あるいは環境別 xcodeproj の Run Script で実行

SwiftGen と自前のコードジェネレーターについては手動実行するようにしました。プロジェクトを clone してきた直後や、差分を取り込んだ後で必要に応じて実行する手間が発生しますが、実行しなければビルドエラーが発生し実行が必要なことに気付けるので一旦これで良いかな、という判断です。

この記事を書いている時点ではまだベータですが、次にリリースが予定されている Xcode 13.3 (Swift 5.6) では Package Manager Command Plugins が使えるようになるため今後は自動実行されるようにしたいと思い調査中です。

LicensePlist と Firebase Crashlytics へのシンボルアップロードについては引き続き Build Phases の Run Script で実行するように設定しました。環境別で分けたことで、この設定を用意した xcodeproj の数だけしないといけないため最初は手間でしたが、設定してしまえばそれ以降そうそう変わるものではないため良しとしています。

移行にあたり問題になったことと解決法

実際に移行をしてみていくつか問題になったことがありました。同じように Swift Package 中心のプロジェクト構成に移行される方が同じ問題に遭遇した時の助けになれば幸いです。

NSKeyedArchiver で永続化したデータの読み込みに失敗

SkyLeap では一部の情報を NSKeyedArchiver を使ってファイルに永続化しています。

今回の対応を行う前に永続化したファイルを対応後のモジュールから読み込もうとしたところ、復元できず失敗に終わっていました。原因はモジュールが変わったことでクラス名が一致せず、ということでした。

この問題は NSKeyedUnarchiver setClass(_:forClassName:) でクラス名と型を紐付ければ、無事新しいモジュールからでもファイルを読み込むことができるようになりました。

NSKeyedUnarchiver.setClass(NewModule.MyClass.self, forClassName: "OldModule.MyClass")

この問題は永続化したファイルの読み込みをテストするコードが既にあったため問題に早く気付くことができました。テストは大事。

テストモジュールのビルド失敗問題

テストコードも Swift Package に移し、いざテストを実行、としたところで CodeSign error が発生する問題に遭遇しました。

エラーメッセージからでは原因がわからなかったのですが、この問題について Stack Overflow に投稿がありました。

原因はビルドした成果物のなかに Resources というディレクトリが含まれているこのようで、ディレクトリ名を変えるなどする必要があるようです。

テスト用のリソースを格納したディレクトリ名が元々 Resources となっており、今回テストコードを Swift Package に移した際、それを Package.swift 上で .copy("./Resources") と記述していたことでディレクトリ自体がコピーされていました。コピーする必要はなかったため .process("./Resources") と修正しリソースがフラットに配置されるようにすることでこの問題は解決できました。

なおきっかけはテストモジュールのビルドでしたが、テストモジュールに限らず起きるものだと思いますのでご注意ください。

移行してみてどう?

今のところはとても快適です。きっかけであった project.pbxproj のコンフリクトは当たり前ですが全く起きません。 Package.swift がコンフリクトするのでは?とも思いますが、起きたとしてもマージは容易なので特に問題となっていません。

とても快適です。(2回目)

というのを当社の社内コミュニティである SwiftWednesday の中で話したところ、いくつか質問があったのでここにも残しておきます。

マルチモジュール化によってビルドが速くなるって言うけど本当?

SkyLeap は元々も構成変更後もマルチモジュール構成としているわけではないので、ビルド時間について大きな変化があったわけではありません。

一方で、テストは Swift Package 側に移したことで、ホストアプリケーションが不要になったことからテストの実行時間には変化がありました。

従来の構成 変更後
1回目 27.7 11.2
2回目 26.2 9.2
3回目 22.1 10.3
4回目 24.2 8.5
5回目 21.3 9.6
平均 24.3 9.76

※単位: 秒

Swift Package 中心のプロジェクト構成にしたことがこの結果となったわけではありませんが、モジュールを分けることとホストアプリケーションを使わないテストにすることで実行時間は大幅に短縮できると思います。

CI/CD 環境での実行は速くなった?

結果から言うと先に書いた通りテスト周りは速くなっています。

SkyLeap は CI/CD 環境には Bitrise を使用しており、テストと社内向けのアプリ配布、それから App Store Connect へのアップロードを Bitrise 上で行っております。

これらのうち、テスト実行については約 30% ほど時間が短縮されました。一方でアプリの配布やアップロードについてはわずかに実行時間が増えていますが、厳密に同じ環境で検証できているわけではないので、構成の変更による影響かどうかはわかりません。機会があればもう少し詳しく調べてみようと思います。

Xcode Project 側に置いたファイルって何がある?

AppDelegate とアプリアイコン用の Asset Catalog、それから Info.plist と環境別の設定ファイル系(GoogleService-Info.plist 等)になります。

Xcode Project

これらとは別に、アプリの動作中に利用可能なデバッグ機能を別途モジュール化し、それを任意の環境の Xcode project 側で依存関係に追加するようにしています。

ここでいうデバッグ機能とは iOSDC Japan 2021 で当社の noppe san が発表している ランタイムデバッグのススメ の中で話されている内容と似たようなものになりますので、もしよろしければこちらもご覧になっていただけたらと思います。

解決できていない困りごと

Swift Package 中心のプロジェクト構成に移行して良いことがある反面、まだ解決できずにいる困りごともいくつかあります。

ファイルヘッダのコメントテンプレート

Swift Package 側で新規にファイル作成をしたときに挿入されるヘッダコメントですが、 Project name や Organization などの定義がないためそのあたりが空っぽのコメントになってしまいます。

//
//  File.swift
//
//
//  Created by qmihara on 2022/03/10.
//

IDETemplateMacros.plist を使用したデフォルトのヘッダーコメントのテンプレートを用意しても良いのですが、SkyLeap 以外の開発もすることを考えると全体に影響してしまうこのやり方はちょっといまいちです。

いい方法をご存知の方がいたらぜひ教えてください。

パッケージグラフの解決に時間がかかることがある

画像などのリソースファイルや Swift ファイルを Swift Package に追加した時に Xcode 上でパッケージグラフの解決処理が行われますが、人によってこの処理に時間がかかりその間 Xcode の動作が非常に重くなるという症状が起きています。

サンプルが少なく全く参考にならない情報ですが、 M1 MacBook Pro で開発しているメンバーの場合は約 3 秒で済むのですが、 MacBook Pro (16-inch, 2019) を使用しているメンバーの場合だと 20~30 秒ほどかかっております。

おわりに

本記事では、実際に運用している iOS アプリのプロジェクト構成を Swift Package 中心の構成に移行したことについて紹介しました。本構成について興味をお持ちの方の参考になれば幸いです。

最後まで読んでいただき、ありがとうございます!
この記事をシェアしていただける方はこちらからお願いします。

recruit

DeNAでは、失敗を恐れず常に挑戦し続けるエンジニアを募集しています。