iOSエンジニアのTerryです、ブログ書くのはとても久しぶりです、Xcode Cloudとても楽しみですね!
今回はCarthage、CocoaPodsで利用している3rdパーティライブラリやプライベートライブラリをSwift Package Manager(SwiftPM)に移行させていって、ライブラリの管理をSwiftPMに一元化していこうとしているお話です。
弊社には2週間に1度タスクデー、大型連休がある時期の1週間程度の期間にタスクウィークという負債の返済や新しい技術のキャッチアップなどを自由にやって良いという日があります、そこを使って少しずつ進めてた作業がひと段落したのでブログ書こうとなった次第です。
今回は
なぜやるのか
どうやったか
大変だった事
今後について
という形で書いていこうと思います。
なぜやるのか
いろいろ理由はありますが1番はXcodeのアップデートがあった時に問題が起こる可能性があることです。 最新の環境で開発したいのに上記理由でアップデートできない状態になってしまうのはとても辛いです。
Appleの公式のライブラリ管理ツールであるSwiftPMに一元化する事でこの問題がなるべく起こらないようにしたいなと考えたのと、
プライベートライブラリの管理を楽にしたいってのがありました。
FammではCarthageでプライベートのライブラリを管理していてそのライブラリの運用に課題を感じていてそれも解消できるのでは?と 考えました。
どうやったか
これをやろうと考えていた当初は単純に
- 3rdパーティー製のライブラリでSwiftPMに対応しているものを修正していく
- プライベートのライブラリをSwiftPMに対応させる
とりあえずサクッと書いたhttps://t.co/SPg6YNh3Rw
— Date (@d_date) 2021年3月24日
と記事
を読んだことがきっかけで、プライベートのライブラリについてはリモートパッケージで使用する予定でしたがローカルパッケージにしてしまおうと方針転換しました。
※これとってもいい記事なのでよかったら是非読んでください and iOSDCでもこの辺のお話をしてくれるぽいトーク
が採択されているのでそれも非常に楽しみですね!
なので、
- 3rdパーティー製のライブラリでSwiftPMに対応しているものを修正していく
- 他のアプリでも共通で使用するものはSwiftPMに対応させてプライベートのリモートパッケージにする
- プライベートのライブラリをFamm本体のローカルパッケージにする
という風にしました。それぞれの作業についてやったことを簡単に紹介していきます。
1. 3rdパーティー製のライブラリでSwiftPMに対応しているものを修正していく
これはまぁ書いてる事そのままなんですが、FammではXcodegenを利用しているのでproject.ymlの- carthage: ${library} の箇所を各ライブラリのSwiftPMの対応状況を確認して対応していたら- package: ${library}に変更してくだけでした。
// e.g APIKit - carthage: APIKit ↓ - package: APIKit
2. 他のアプリでも共通で使用するものはSwiftPMに対応させてプライベートのリモートパッケージにする
TimersではFamm以外にもFamm年賀状というアプリがあって、UIやAPIの実装など共通する箇所もあるのでその辺に関わるライブラリをSwiftPMに対応させました。具体的なやり方はここでは割愛しますが
該当のライブラリにPackage.swiftを追加して必要な情報をそこに書いていくという形で対応します。
あとは1と同様carthage → SwiftPMにproject.ymlを変更しました。
3. プライベートのライブラリをFamm本体のローカルパッケージにする
最後に、ここが一番大変だったので少し長くなります。 2でやったもの以外の残りのプライベートライブラリをFamm本体のローカルパッケージにする作業ですが、
Fammにローカルパッケージ用のディレクトリを作成して
Package.swiftを追加aに各ライブラリのソースコードのファイルを追加
bで追加したファイルを利用できるように
Package.swiftに追記していくFammで利用できるように1と同様carthage → SwiftPMに
project.ymlを変更していく
になります。
ライブラリごとに1つずつパッケージ化する選択もありますが今回はローカルのパッケージ名にFammLibraryという名前をつけ、その中に各ライブラリを追加する形にしました。
こんなかんじ

今回対応した内容としては以上です。
大変だった事
やったことを簡単に書きましたが大変だったこともあったのでそれをいくつか紹介したいと思います。主に上の3. プライベートのライブラリをFamm本体のローカルパッケージにするに関わる事なのですが、
- ライブラリごとに対応したかったけどできなくて結局まとめてやったのでプルリクがとても大きくなってしまってレビュワーの負担がとても大きかった
- UIKitを利用しているファイルで
import UIKitされてないとダメ、ゼッタイ - Objective-Cのコードで書かれているライブラリは混在できない問題
- リソース(XIB, plistなど)周り
1. ライブラリごとに対応したかったけどできなくて結局まとめてやったのでプルリクがとても大きくなってしまってレビュワーの負担がとても大きかった
既存のライブラリをローカルパッケージに移行する際に当初は1つずつ対応していけばいいかと思ってやりはじめたのですが、プライベートライブラリが別のプライベートライブラリに依存してたりがたくさんありました。
それをまずリモートでSwiftPMに対応してそこからさらにプライベートライブラリ化してみたいな事が今思えばできたのですがその時はその方法が思いつかず、「いろいろごちゃごちゃになって出来ねぇ」って気持ちになってしまっていたのでレビュワーになりそうなメンバーに「ごめん、プルリク大きくなる」ってのを先に伝えて了承してもらい(感謝🙏)一気にガッとまとめてやりました。
2. UIKitを利用しているファイルでimport UIKit されてないとダメ、ゼッタイ
iOSアプリなどのプロジェクトではimport UIKitされていないファイルでもUIKitを利用できますがSwiftPMではそれを許してくれません、エラーになった箇所(大抵はimport Foundationになっている)をimport UIKitに置き換えていくのですがそのままで良いものもあるので一つずつ目視で確認してちまちま修正していきました。
3. Objective-Cのコードで書かれているライブラリは混在できない問題
プライベートライブラリにはObjective-Cで書かれているものもあり、最初は特に何も考えずに同じFammLibraryに入れていたがうまくいかなかった。少し調査した感じだとSwiftPMとObjective-Cを同じパッケージにライブラリとして混在させる事ができないっぽい(しっかりと調査したわけではないので出来るのかもしれないですが)。
これはObjective-Cのライブラリは別ライブラリとして分けることで解決しました。 また、Objective-CのライブラリはC Language Targetsとして作成しています。
4. リソース(XIB, plistなど)周り
もろもろの作業が終わったので端末にインストールして動作確認をしていると色々な箇所でクラッシュしました、ログを見るとどうやらプライベートライブラリ化したものにあるリソースファイルが読み込めなくてクラッシュしていて、これの解決にはiOS用ライブラリ作成者向けSwift Package Managerのリソース周りTipsという記事がありとても助けられました。
ポイントとしては
- XIBのCustom classのModuleを指定する
Bundleの出し分けをする- 自動的に入らないリソースについては
resources: [.copy(“${リソースのパスを指定}”)]などでPackage.swiftに追記する
あたりが特につまづきポイントでした。
今後について
以上の対応でめでたくビルドもできクラッシュもしなくなり、すでに本番環境のアプリとしてリリースされています🎉 今後についてはまだ対応できていない部分もあるので順次対応していきたいのと、isowordsのように機能ごとにパッケージに切り出していくみたいなこともやっていけたらいいなと思っています。
結構ながくなってしまいましたが最後まで読んでいただきありがとうございました! もっとこうした方がいいよとかフィードバックありましたらいつでもお待ちしています!
おまけ
isowordsのPackage.swiftもそうなんですが一つのパッケージに複数のライブラリがあるとPackage.swiftが少し読みづらく、どうなってるのか把握しづらかったです。なのでFammLibraryのPackage.swiftでは以下のようなenumを定義して
enum FammLibrary: CaseIterable {
case libraryA
case libraryB
case libraryC
.
.
.
case libraryH
func append(on package: Package) {
package.products.append(contentsOf: [
.library(name: libraryName, targets: [targetName]),
])
package.targets.append(contentsOf: [
target,
])
}
var libraryName: String {
switch self {
case . libraryA:
return “LibraryA"
.
.
.
case . libraryH:
return “LibraryH”
}
}
var targetName: String {
switch self {
case . libraryA:
return “LibraryA"
.
.
.
case . libraryH:
return “LibraryH”
}
}
var target: Target {
let target = Target.target(name: targetName)
// Add dependencies
switch self {
case . libraryA:
target.dependencies = [
“LibraryB”,
“LibraryC”,
]
case .libraryH:
target.dependencies = [“LibraryC"]
target.resources = [
.copy(“hoge”)
]
default:
// nothing todo
break
}
return target
}
}
以下のようにして追加するようにしています。
FammLibrary.allCases.forEach { library in
library.append(on: package)
}
これでわかりやすくなったかどうかは人それぞれだと思いますが、ライブラリの追加があった場合などにswitch文で網羅してるので追加漏れを一定防ぐ効果はあるんじゃないかなと思っています。
積極採用中!!
子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中! 急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください! 採用HP: http://timers-inc.com/engineerings