テストのときだけ任意のモジュールをインポートする方法

鈴木大貴氏:次の項目に進みます。mockやfakeなどテストのときだけ必要なものなど任意のモジュールをimportしたいという需要はあると思います。

KMMの構成です。moduleAというかたちでKMMのプロジェクト側で定義されたときに、iOSKMM.framworkというフレームワークが1個生成されます。

モジュールが複数あった場合は、そのモジュールをまとめるモジュールを1つ用意して、まとめたモジュールをビルドすればiOSのフレームワークとして出力できます。

さらにそこに対してmockなどテストのときにしか使いたくないモジュールを別なモジュールとして定義して、それにアプリ側で使うものを紐づければ、2つのフレームワークとしてKMMのフレームワークとmockのフレームワークを生成できます。

ここで注意しないといけないのが、KMMのmockのフレームワークに対してiOSのもともとのKMMのフレームワークがリンクされていないということです。iOSKMMMockでiOSKMMをimportしている状態にはなっていないです。

実際にフレームワークから見ると、まったく同じものを別モジュールとしてビルドしたものが、それぞれできあがるというかたちです。

なので、例えばApiClientというクラスがmoduleAにそれぞれあったとしても、iOSKMMとiOSKMMMockでは型が別なものという扱いになります。

今回はCocoaPodsを使ってこれを取り込んでいるので、これは妥協案なんですが、iOSのconfigurationのRelease向きかDebug向きかというもので設定ができます。

テストするときには基本的にDebug configurationを利用して、ストアにリリースするときのアプリに対してはRelease configurationを利用します。そのconfigurationで紐づけるフレームワークを変えることで、一応利用できます。

厳密に言うと、テストのときだけ利用したいものをimportするのではなくて、開発のときにだけテストで利用するものも含められるようにするという対策をしています。

この方法じゃなくても、テストターゲットのビルド時にリンクするフレームワークを変更するスクリプトを書けば使えなくはないのですが、そのままCocoaPodsの機能として使っています。

KMMのフレームワークをCocoaPodsでダウンロードする

その延長で、実際にKMMのフレームワークをCocoaPodsのプライベートリポジトリのReleasesにアップロードして利用できるようにする方法です。

公式でアナウンスされているものは、native.cocoapodsというプラグインを入れて、Gradleの設定ファイルの中にCocoaPodsの設定を書いて、それらを同一の階層にいるものとみなしてpathで指定して取得するという方法です。

ビルドしたものを都度iOS側で取り込むのではなくて、リンクされたものをスクリプトでビルドしてiOS側で参照します。

今回実現している方法はそうではなくて、Kotlin Multiplatformで生成したものをzipでプライベートリポジトリのReleasesにアップロードをして、Podspecを自分自身で定義して、それをプライベートなSpecsにアップロードして、Xcodeから取り込む場合には、CocoaPodsでPrivate SpecのPodspecを指定して実際の成果物をダウンロードするというものです。

これを通常使うPodspecの定義で定義すると、このようにダウンロードのzipのリンクを定義することになると思います。プライベートリポジトリのReleasesだとWebサイト上はアクセスできるんですが、実際にそのリンクから直接アクセスしようとすると、認証状態だったとしてもそもそもアクセスするURLが違うのでエラーになってしまいます。

なので、すごく長くなっちゃうんですが、さっきの定義していた部分がこの赤い部分だとして、Podspec自体が定義ファイルではなくて実質Rubyのコードを書けるスクリプトになっているので、assetのURLを取得するものを含むかたちにしています。

動きとしてどうなっているかというと、podのrepo pushでプライベートなspecsにpushします。specの中で実際にGitHubのAPIに該当tagの情報を取りに行って、そのReleasesの情報の中から該当するzipファイルの名前を取得して、そのassetのURLを取得してspecのsourceの中に入れるという処理をしています。

これは疑似的な定義ですが、先ほどの長い定義ファイルの中で外側から見えるのはこのsourceで、GitHubのassetsのダウンロード先のURLが取得できます。

そのままアクセスするとzipではなくて、JSONとして定義の情報を取ってくるだけなので、application/octet-streamをheaderで指定して、typeはzipで取ってきます。

Private Specのテンプレート自体はGistで公開されているので、ぜひ見てもらえればと思います。

ちょっと多いんですが、Releasesを利用するときの注意点を簡単に上から話します。pod installのときにspec自体はインストール可能な状態なんですが、lintをかけようとするとvalidationのエラーになるというものがあるので、現在はそれを実行しない方法で対策を取っています。

外部のFacebook SDKでもspecのlintを試してみたらエラーになったので、これはおそらくまだ修正されていない問題なんじゃないかなと思っています。

ダウンロードするときにはGitHubのACCESS_TOKENが必要になんですが、pushのオプションで--use-jsonを使うとACCESS_TOKENが先ほどのJSONファイルに書き込まれてしまうので、netrcを使って認証するのをおすすめします。

また、xcftameworkを使うと実際にリンク可能なものでもpod repo push時に内部で走っているlinkerのテストでエラーになる場合があるので、importのvalidationとtestをskipすることで現状はいったん回避しています。

あとpod installの時に実際にpodでpushしたときもnetrcかGITHUB_ACCESS_TOKENを使っていた場合にはどちらかを利用しなければなりません。ここまでがCocoaPodsの導入の仕方です。

Kotlin側のサブクラスのswizzleを回避してクラッシュ問題は解決

最後に、あまり該当する人はいないとは思いますが、Kotlinで実装されたNSObjectのサブクラスをSwizzleするとクラッシュするという問題があります。

サードパーティのフレームワークなどで、iOSだとSwizzlingという、もともと定義されているメソッドを書き換えるものがあるんですが、それが使われているとクラッシュするということが発生しています。

これはkotlin-nativeの1.4.30では直ってはいるので、バージョン的に使えないという方向けに説明します。参考の実装として、APIを通信する部分でktorというものを使っています。

ktorにはIosResponseReaderというものが実装されていて、それがNSObjectのサブクラス、かつURLのDelegateを実装しています。

Delegateのメソッドがこのようなかたちで4つほど定義されています。このメソッドがパフォーマンス計測などでSwizzlingされることがあります。

これらがIosClientEngineというもので実際にktorの中で定義されているんですが、iOSのNSURLSessionをKotlin側で初期化されたときにresponseReaderをDelegateとして渡す状態で使われています。

responseReader自体がinternalで定義してあるので、なにか修正を加える場合にはforkして対応するか、そのままコピーして修正を加えるかの方法でしか対策は取れなくなります。

今回の例に関しては、Swift側でURLSessionのinitializer自体をswizzleできれば、Kotlin側のサブクラスをswizzleすることにはならないので、delegate自体を入れ替えます。

swizzleをして、クラスの名前がKotlinに関連しているktorのものだったら入れ替えをして、1個ラップしたdelegateで実装します。

delegateの実際の内容は、先ほど定義したdelegateメソッドをもう1度Swiftで定義し直して、実際にKotlinに定義されているものを1個ラップして、愚直にメソッドをもう1度呼び直すというものです。

直接Kotlin側で定義されたNSObjectのswizzleが走るわけではないので、クラッシュを回避できます。 これはiOSのswizzleするときのメソッドです。

私からはiOSに導入する部分でいろいろと話をしました。ご清聴ありがとうございました。