はじめまして、iOSエンジニアの阿久津 @sky_83325 です。
タイミーでは、機能ごとにEmbedded Frameworkに分割して開発するマルチモジュール開発に取り組んでいます。 現在では、本体AppやAppExtensionの他に7つの共通Framework、そして16個の機能Frameworkという規模になってきました。
今回は、そのマルチモジュール開発をEmbedded Frameworkではなく、Swift Packageを利用した方法に乗り換えてみたので、その成果や学びについて共有できればと思います。
取り組んだ経緯・背景
タイミーでは、技術顧問の@d_dateさんと隔週で「ツバメの会」という情報共有の場を設けています。そこでは、直近タイミーで取り組んでいることの共有や相談をしたり、SwiftやiOS、その他エンジニアリングの最近の話題について議論したりしています。
そのツバメの会で、pointfreeがOSSとして提供しているisowordsというアプリが取り上げられました。
そのisowordsは86ものモジュールから構成されていますが、そのモジュールをSwift Packageとして管理しています。
Swift Packageとして管理することで、ファイルの追加や変更のたびに project.pbxproj
が変更される辛みからも解放され、結果としてXcodeGenのようなツールを利用してプロジェクトファイルを生成しなくても、十分に管理することが可能となります。
この構成に大きな魅力と可能性を感じたため、タイミーでもSwift Packageを利用したプロジェクト構成への変更に舵を取りました。
この記事では、次の内容を共有します。
- Swift Packageに移行したことで得られた成果
- どのように移行していったのか
- 実際に運用してみた感想
Swift Packageに移行したことで得られた成果
1. XcodeGenを取り除くことができた
タイミーでは次の2つの目的のために、XcodeGenを導入していました。
project.pbxproj
のコンフリクトを防ぐ- フレームワーク追加の処理をテンプレート化する
特に前者は、多くのプロジェクトでXcodeGenが導入されている理由ではないでしょうか。
複数人で開発を行なっていると、ファイルやディレクトリの追加に伴い、プロジェクトファイルがコンフリクトしがちです。常にプロジェクトファイルを生成するようにし、プロジェクトファイルを.gitignoreに追加することでコンフリクトの発生を抑えることができます。そういった目的でタイミーではXcodeGenを利用していました。
また、Frameworkターゲットを追加するたびに、毎回同じ作業が必要でしたが、その作業を誰でも同じように行えるようにテンプレート化するためにXcodeGenを利用していました。
XcodeGenの利用に問題があったわけではありません。従来のプロジェクト構成においては、XcodeGenを利用してプロジェクトファイルは常に生成する運用で特別な問題は発生していませんでした。
しかし、Swift Package Manager(以下SwiftPM)を利用し、ソースコードをSwift Packageとして管理することで、Xcode Projectにはファイルやフォルダへの参照が発生しません。これにより、Build Settingsの変更やTargetへの依存の追加を除いては、 .pbxproj
に変更差分は発生しません。
そして、新しくモジュールを追加する際の処理も、 Package.swift
を少し編集するだけとなったので、テンプレートなども不要になりました。
こうして、XcodeGenを取り除くこととなりました。
project.pbxproj
は16416行から2295行へなり、XcodeGenのymlも削除されました。
// Before I'm working on timee-ios $ wc -l timee-ios.xcodeproj/project.pbxproj 16416 timee-ios.xcodeproj/project.pbxproj // After I'm working on timee-ios $ wc -l App/timee-ios.xcodeproj/project.pbxproj 2295 App/taimee-ios.xcodeproj/project.pbxproj
// Before I'm working on timee-ios $ wc -l XcodeGen/* 102 XcodeGen/common-targets.yml 189 XcodeGen/project.yml 47 XcodeGen/sandbox-targets.yml 179 XcodeGen/scene-targets.yml 58 XcodeGen/setting-groups.yml 73 XcodeGen/templates.yml 178 XcodeGen/unit-test-targets.yml 826 total // After I'm working on timee-ios $ wc -l Package.swift 272 Package.swift
2. 本体Appのビルド時間が90秒から10秒になった
Swift Packageに移行した結果、ビルド時間が大幅に改善されました。
これはSwift Package化したことによるものなのか、従来のプロジェクト構成に無駄があったのかはわかりませんが、思わぬ恩恵を受けることになりました。
追記(2021年9月1日) SwiftPM対応した際に、BuildScriptで実行していたSwiftLintが動かなくなっていました。 そのSwiftLintの実行時間が40秒ほどかかっていたため、それが外れたことによりビルド時間が大幅に削減されていました。
3. アプリのサイズが30.3MBから26.9MBになった
こちらも当初は期待してなかった成果でした。
4. アプリの起動時間
Embedded FrameworkはDynamic Linkされていたので、今回Static Frameworkとしてリンクされることで起動時間が早くなるかもと思っていました。
今回実測はしていませんが、肌感では初回起動含め特に変化は感じていません。
5. その他
プロジェクトのセットアップがかなり容易になりました。まだCocoaPodsを併用していたり、Swift製のコマンドラインツールの管理にMintを利用しているため、Git Clone後にXcodeを立ち上げそのまま開発を始められるという状況にはなっていません。
ですが、都度XcodeGenのscriptを走らせる必要がなくなったのは大きな開発体験の向上だと感じています。引き続き、MintのMintのSwiftPM移行などを進めていき、Xcodeのみで開発できる状況を目指していければと思います。
どのように移行していったのか
それでは、どのようにプロジェクト構成を移行していったのかを見ていきます。
既存のタイミーの構成は次の通りでした。
- 外部ライブラリの管理は基本的にSwiftPMを使用して行っているが、一部CocoaPodsを利用している
- 1つのプロジェクトで、Appや複数のEmbedded Frameworkを管理している
- xcworkspaceを用いて、Podsとメインprojectを紐づけている。
移行後のゴールとしてはisowordsを参考に以下のように定めました。
- XcodeGenを取り除く
- ローカルライブラリ及びリモートライブラリはSwift Packageとして管理する(メインprojectではSwift Packageのビルド成果物であるFrameworkをリンクして利用する)
- 一部SwiftPM未対応のものはCocoaPodsを利用する
- xcworkspaceでメインproject、Pods、Swift Packageを編集可能にする
このゴールを達成するために、次の手順で移行を行いました。
- XcodeGenを取り除く
- xcworkspaceの構成を整える
- Embedded FrameworkをSwift Packageに切り出す
- CocoaPodsを再度導入する
1. XcodeGenを取り除く
まず、XcodeGenを取り除くということを大きな目的の1つにしていたので、この作業から始めました。具体的にはgitignoreの整理と、XcodeGenに関する記述の削除のみです。
.pbxproj
はXcodeGenを利用し都度生成していたため、Gitでは管理していませんでしたが、管理するように変更しまし、差分を確認できるようにしました。
2. xcworkspaceの構成を整える
従来の構成は、次の画像のようになっていましたがisowordsによせるため、Rootにおいていた各種ファイルを /App
以下に配置しました。
それにより、xcworkspaceからxcodeprojに対する参照がなくなってしまうため、 ***.xcworkspace/contents.xcworkspacedata
でpathを更新します。
ワークスペースのインスペクタに何を表示するかは、この contents.xcworkspacedata
によって管理されています。
<?xml version="1.0" encoding="UTF-8"?> <Workspace version = "1.0"> <FileRef location = "group:App/taimee-ios.xcodeproj"> // ここのpathを更新する </FileRef> <FileRef location = "group:Pods/Pods.xcodeproj"> </FileRef> </Workspace>
ここまでの変更は、SwiftPMなどは一切関係なく単にディレクトリ構造を変更しただけです。 ここで、一度ビルドが通ることを確認し、前準備を完了とします。
その後、プロジェクトのRootで swift package init
を実行し、Swift Package導入の準備を進めます。
I'm working on timee-ios $ swift package init Creating library package: timee-ios Creating Package.swift Creating Sources/ Creating Sources/timee-ios/timee-ios.swift Creating Tests/ Creating Tests/timee-iosTests/ Creating Tests/timee-iosTests/timee-iosTests.swift
同様に、こちらもworkspaceで管理したいため、 contents.xcworkspacedata
を編集していきます。
今回は、rootディレクトリの内容を全て表示したいため、 group:
を指定します。
<FileRef location = "group:"> </FileRef>
ここまでで、Swift Packageを導入する準備が整いました。 この時点での構成は次の通りです。
補足
group:
を指定しただけではApp/
の内容もSwift Package側に表示されてしまいます。非表示にするには、非表示にしたいディレクトリの中に空のPackage.swift
を作成することで防げます 参考App/
以下に本体projectを移行したことで、各種ツールのPathが壊れるかもしれないので適宜修正します。- 以降の作業では一時的にCocoaPodsで導入しているライブラリの参照を外して作業を進めています。CocoaPodsがプロジェクトにリンクされている状態で移行を進めると、ある不具合が発生した場合にSwiftPM依存の不具合なのかCocoaPodsによる不具合なのか判断しにくいと考えました。
3. Embedded FrameworkをSwift Packageに切り出す
ここまで来れば、あと一息です。 Swift Packageに対応する準備は整ったので、Embedded FrameworkをひとつひとつSwift Packageに移行していきます。
具体的には以下の作業を順に繰り返していくことになります。
Sources/
Tests/
にFrameworkのコードを移行するPackage.swift
に移行したFrameworkを定義するPackage.targets
にtargetとtestTargetを追加する- 外部ライブラリが必要であれば
Package.dependencies
を定義し、Package.target
のdependencyに追加する Package.products
に.library
を定義する。これによりアプリターゲットにFrameworkをリンクすることが可能となる。
- Xcode ProjectのTargetから不要なものを取り除く
- Xcode ProjectのTargetに2で定義したFrameworkをリンクする
例として、共通コードをまとめたFrameworkである Common
の場合を次に示します。
各種dependencyを指定しtargetを作成したあとに、productsでlibraryを提供することで、本体projectで Common
というモジュールとFrameworkを使えるようになります。
// swift-tools-version:5.3 import PackageDescription let package = Package( name: "Frameworks", platforms: [ .iOS(.v13) ], products: [ .library(name: "Common", targets: ["Common"]) ], dependencies: [ .package(name: "Lottie", url: "https://github.com/airbnb/lottie-ios", from: "3.0.0"), .package(name: "RxSwift", url: "https://github.com/ReactiveX/RxSwift.git", from: "6.0.0"), .package(name: "Kingfisher", url: "https://github.com/onevcat/Kingfisher", from: "6.0.0"), .package(name: "ImageViewer", url: "https://github.com/Taimee/ImageViewer", from: "0.2.0"), ], targets: [ .target( name: "Common", dependencies: [ "Umbrella" ]), .target( name: "Umbrella", dependencies: [ .product(name: "ImageViewer", package: "ImageViewer"), .product(name: "Lottie", package: "Lottie"), .product(name: "Kingfisher", package: "Kingfisher"), .product(name: "RxSwift", package: "RxSwift"), .product(name: "RxRelay", package: "RxSwift"), .product(name: "RxCocoa", package: "RxSwift") ]), .testTarget( name: "CommonTests", dependencies: [ "Common" ]) .testTarget( name: "UmbrellaTests", dependencies: ["Umbrella"]), ] )
同様に、全てのFrameworkを Package.swift
を編集しながら移行していくことになります。
単調な作業が進みますが、 .pbxproj
がどんどん軽量になっていくことを実感できます。
補足
import UIKit
やimport Foundation
などの記述が漏れているとビルドに失敗するので適宜修正しましょう- BundleはPackageごとに作成されるので
Bundle.module
を利用しましょう
4. CocoaPodsを再度導入する
最後にCocoaPodsを再導入していきます。既にxcworkspaceは作成済みであるため、既存のxcworkspaceを使うようにPodfileで指定します。それ以外は、特に気をつけることはありません。
workspace 'timee-ios' project './App/timee-ios.xcodeproj'
これで無事以降が完了しました🎉
実際に運用してみた感想
実際にSwiftPMを中心としたプロジェクト構成にして、1ヶ月弱運用してみてメリット/デメリットが分かってきました。 メリットに関しては、この記事の前半で述べたように、基本的にXcodeを開くだけで開発を始められる体制になったということです。ビルド時間の高速化もかなり大きなメリットでした。
他方、良い面ばかりではなく、比較的新しい技術であるため、いくつかの不具合なども観測されました。具体的には以下の通りです。
不具合だと思われるもの
Swift Packageにファイルを新規追加するのが少し辛い
CocoaTouchClass
を新規追加する際、UIViewController
などを指定することが出来ません。通常はNext
を押した後の画面で、親Classなどを指定できると思うのですが、Next
を押すとすぐにObjective-C用のファイルが作成されます😇 一時的な不具合だと思うので、次のXcodeに期待です。AppExtensionを利用している場合、binaryTargetを利用することが出来ない
AppExtensionを利用しているアプリで、SwiftPMを利用してbinaryFrameworkを利用する場合にAppStoreConnectにアップロード出来ない不具合があります。AppとAppExtensionの両方に対してFrameworkがコピーされてしまうため、AppStoreConnectへアップロードする際に、Bundleの重複でTransporterErrorが出ます。手動で片方を削除するなど対応可能かもしれませんが、CIなどでリリースプロセスを組んでることが多いと思うので、現時点では利用不可と考えるのが無難かもしれません。タイミーでも、これらはCocoaPodsで導入することで対応しました。 forums.swift.org
InterfaceBuilderからソースコードにIBOutlet/IBActionを接続しようとするとクラッシュする
これは少し開発体験を損ねています😢ソースコード側からIB側に接続することは可能なので、そのようにして対応しています。次のXcodeで修正されると期待します。
これらの不具合はSwiftUIを使えば解決出来ることでもあるので、早めに乗り換えていきたい気持ちが高まりました。
その他
Swift Package内でCustomBuildConfigurationを利用することが出来ない
Swift Packageでは、標準の
RELEASE
DEBUG
以外は利用できません。そのため、なんらかの対応が必要です。環境ごとにプロジェクトを分けてしまうのが良いかもしれません。 forums.swift.org— Date (@d_date) 2021年4月21日
最後に
Swift Package中心のプロジェクトに移行しましたが、まだまだやりたいことが沢山あります。 タイミーでは、技術的挑戦を一緒にしていけるメンバーを常に募集していますので、少しでも興味がある方はぜひご連絡ください。
その他参考文献