🎀

iOS SDKをSwift Packageで配布して学んだこと全部乗せ(XCFramework対応)

に公開

XCFramework + Swift PackageでSDKを配布する構成とその知見

株式会社バニッシュ・スタンダードでモバイルアプリのエンジニアをしている、@ohayoukenchanです!

今回、会社のテックブログの執筆機会をもらったので、せっかくなので直近の業務の話ができればと思います

今回書く内容

他アプリに組み込むことができるSDKをSwiftPackageを使って配布する話です

普段はネイティブアプリの開発に従事しているのですが、直近半年ほどiOSのSDK開発に携わり、さまざまな知見を得たのでSDK開発をなぜオススメしたいか、お伝えできればと思います🎉

こんな方にオススメ

  • 業務でモバイルアプリ開発に携わっている
  • レポジトリを複数のエンジニアで共同作業している
  • ソースコードが読みにくくなってきている

提供しているSDKについて

どんなSDK?

弊社のSTAFF STARTというサービスをご利用いただいている企業のスタッフが、自身のブランドのコーディネートなど着用した写真や動画などをアップできる機能があり、今回は、そのコーディネートやスタッフを表示する機能をSDKで提供しました。

詳細はこちらをご確認ください

SDKの配布方法と構成

その前に、ざっくりSDKがどのような機能を提供するかお伝えします。StaffStartCoreはSDKの共通処理を担当します。FirebaseなどのSDKの初期化でよくあるFooApp.initialize()などの処理が含まれます。StaffStartAppはスタッフさんの情報や、コーディネート写真などを取得し、一覧画面などのUIを提供します。UIはSwiftUIを利用しています。StaffStartTrackingはpvなどの計測に用います。

モジュール 役割
StaffStartCore SDKの初期化など
StaffStartApp APIの問い合わせや、画面提供
StaffStartTracking 計測まわり
CoreModule すべてのモジュールで利用する共通基盤

なぜXCFramework + SPMにしたのか?

Swift packagesが登場してもうだいぶ経つので、配布はSPMで公開するだろうと当初は思っていました。外部クライアント様とのすり合わせでもSPMで確定と思っていたのですが。。。違いましたw

公開用リポジトリの構成

SDKの公開はSPMで行っていますが、配布している成果物はソースコードではなく、XCFrameworkをlibとして提供しています。イメージとしては下図です。

なぜこのような構成になっているのかというと、ソースコードは公開したくないという事情がありました。

iOSアプリに組み込まれる想定なので、GitHubリポジトリをpublicにして、ソースコードを公開するなら全てSPMで管理できそうだったのですが、事情があるのでこのような構成になっています。

ですので、実際の作業はprivateなリポジトリで行われ、publicなリポジトリにはXCFrameworkのみ配置する構成になっています

プライベートリポジトリの構成

まずは構成図を貼ります

SPMをガンガン使っており、こちらもざっくり概要を書きます。

モジュール 役割 形式
StaffStartCore SDKの初期化など Framework (Xcode project)
StaffStartApp APIの問い合わせや、画面提供 Framework (Xcode project)
StaffStartTracking 計測まわり Framework (Xcode project)
CoreModule すべてのモジュールで利用する共通基盤 lib (dynamic)
Layered Module APIやViewなどをInfrastructure、Presentationに分けるレイヤードアーキテクチャを格納する lib(static)
UILibrary デザインシステムやアセットの格納場所 lib(static)

モジュールごとの形式と依存関係

StaffStartCoreなどのimportしてアプリケーション側で利用するコードのみアクセスレベルをpublicにしてアプリケーション側から見えるようにしています。SDK内部のロジックを隠蔽する理由もありますが、単純にSDKとして利用できるI/Oを制限したほうがいいと思ったからです。普段アプリを書く際はそれほどアクセスレベルに目を向けていなかったのですが、何を提供して、何を隠蔽するのかという新しい視点が生まれ、アクセスレベルを気にする機会が増えたのはSDK開発のとても良いところだと思います。

Layered ModuleはStaffStartApp,StaffStartTrackingの両方で使っていますが、それぞれ吐き出しているlibが違います。StaffStartAppにはUI描画に必要なモジュール、StaffStartTrackingでは計測まわりで必要なモジュールになっています。

一部抜粋となりますがPackageの中身は以下のようになっています。AppPresentationはlibとして吐き出され、StaffStartAppに取り込むようになっています。AppInfraStructureはdependenciesとして指定しており、これでStaffStartAppからAppInfraStructureは見えないようになっています。こうすることでStaffStartAppからInfraレイヤーの操作はできないようになっています。

    products: [
        .library(
            name: "AppPresentation",
            targets: ["AppPresentation"]
        ),
        .library(
            name: "TrackingPresentation",
            targets: ["TrackingPresentation"]
        )
    ],
...
    targets: [
        .target(
            name: "AppPresentation",
            dependencies: [
                .appInfrastructure,
                .appCore,
                .coreModule
            ],
            path: "Sources/App/Presentation"
        ),
        .testTarget(
            name: "AppInfrastructureTests",
            dependencies: [
                .appInfrastructure
            ],
            path: "Tests/App/InfrastructureTests"
        ),
    ...
    ]

dynamicライブラリにまつわる落とし穴

重複シンボルの回避とdynamic化の必要性

同じようにCoreModuleもStaffStartCoreLayeredModuleに依存されるようになっています。ここで、CoreModuleをstaticライブラリとして依存させると、StaffStartCoreLayeredModuleの双方にCoreModuleがコンパイルされてしまいシンボルが2つできてしまいます。(これは Xcode ビルド時に複数モジュールが同じ .o ファイルをリンクしようとすると起きる「重複シンボル問題」によるものです
こうなってしまうとアプリケーション実行時に該当クラスが呼び出されたときにどちらのシンボルを利用していいか分からず、アプリがクラッシュしてしまうおそれがあります。こちらは回避策として、CoreModuleをdynamicライブラリとして吐き出しています。そうすることで、コンパイル時にはCoreModuleは依存先には組み込まれず、必要になったタイミング(各ライブラリがdynamicライブラリをリンクしにいくとき)に初めて読み込まれる形になります。

ただしlibをdynamicにするときは注意が必要です。

Previewが動かないくてハマる

LayeredModuleでSwiftUIのPreview表示が不安定になってしまいました。理由は、Xcode Previewが起動する際のビルド・実行プロセスが通常のアプリビルドとは異なり、モジュール単体で動作するためです。

このとき、LayeredModuleが依存しているCoreModuleがdynamicライブラリとしてビルドされていると、Previewプロセスの中でCoreModuleを正しくロードできず、Previewがクラッシュするなどの問題が発生します。

これはPreviewの仕様上、実行時リンクのパス解決が難しくなるためで、Preview対応を安定させるには、Preview専用の構成を用意するか、staticリンクに切り替えるなどの工夫が必要になります。

ワークアラウンドと現状の対処

まだこちらの対応はできておらず、今もPreviewが動かなくなるときがあります。そのときはPreviewに使うSimulatorを変えると表示されたりするので現状そのようにして対応してます(めちゃめちゃワークアラウンドですが)

まとめ

SDKを配布する方法を検討する中で、Swift Package Manager(SPM)やXCFramework、さらには static|dynamicライブラリの使い分けについて深く理解することができました。

特に、ソースコードを公開せずにSPM経由で配布したいという要件に対して、XCFrameworkをSPMで配布するという構成はとても有効でした。実際この構成になるまで4度設計を見直し現在はver4ですw。

また、ライブラリをdynamicで扱うことによるシンボルの重複回避やリンクタイミングの違いなどは、実際にクラッシュやPreview不具合といったリアルなトラブルに遭遇しながら理解が深まりました。

Preview が動かない原因を探ったり、アクセスレベルを見直したりといった取り組みは、普段アプリ開発だけをしていると意識しづらい部分であり、SDKという外部に提供する視点を持つことで設計への向き合い方が変わることを実感しました。

SDK 開発を通じて、「どうモジュールを設計し、どこまで公開するのか」というアーキテクチャレベルの設計力が自然と鍛えられたと感じています。

[本編ココマデ]

引き続き頑張るぞい!

✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁ キリトリ線 ✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁✁

番外編🍡

本記事の内容に関連して何個か以前に投稿した内容のリンクを貼っておきます!SDK開発のお供にお役立てください!

Swift DocCを使ってドキュメンテーションを公開した

SDKといえばドキュメントですよね!アプリケーションを使う側からすると、何もドキュメントがないとどのように使用すればよいのか分からないのでドキュメントを書きました。

こちらで公開しています。

SwiftDoccについてはこちらで詳細にとりあげていますので合わせてご確認ください
https://zenn.dev/ohayoukenchan/articles/69ac2b4cf370e6

フォントサイズなどにDocCコメントを書いておけば、実装時にいちいちソースコードを探しに行かなくてもどの値が何ptなのかを教えてくれるので便利です!

フルSwiftUIにおける無難なアーキテクチャを探した

フルスクラッチでコード書いたので、その中で産まれたアーキテクチャについてこちらの記事で取り上げていますので、合わせてご確認いただければと🙏

https://zenn.dev/ohayoukenchan/articles/b3f7bc52e289a9
https://zenn.dev/ohayoukenchan/articles/5fff5341741d35

株式会社バニッシュ・スタンダード

Discussion