LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

KMPをknewのiOSアプリに導入した際にハマったこと

knewでiOSエンジニアをしている伊原です。

knew.jp

knewではモバイルアプリにKMPの導入を進めています。
KMPをiOSアプリに導入するにあたり、ハマった点がいくつかあったので備忘録として紹介します。

KMPとは?

KMPはKotlin Multiplatformの略で、Kotlinを利用してiOSやAndroid等のマルチプラットフォーム間でのコード共有を可能にする技術です。 KMPを利用すると、iOSアプリとAndroidアプリで共通の処理を1つのコードで書いて共有するといったことが出来ます。

kotlinlang.org

KMPをknewに導入した経緯

knewにはiOSとAndroidのアプリがあるのですが、APIやデータベース周りの処理等、各OSで似たような処理も多く、新規の開発や修正がある度にそれぞれのOSで対応を行う必要がありました。

また、iOSとAndroidで実装されている仕様にも若干のばらつきがあり、それらを解消するために度々作業が発生していました。 そこで上記の問題を解消する為に、KMPを導入してみようという話になりました。

KMPをどのように導入したか

knewではクリーンアーキテクチャを採用しています。 UIの実装はすでにiOS/Androidそれぞれで実装されており、OS独自のUIコンポーネント等もある為、ViewModelより下層のレイヤーをKMPで共通化しています。

下記画像の水色の枠の部分を各OSでの実装からKMPの共通コードに置き換えるようなイメージです。

knewのiOSアプリの構成(KMP導入前)

KMPをiOSアプリに導入した時にハマったこと

iOSアプリにKMPを導入した際にいくつかハマった点があったので、ここで紹介します。 KMPのメインの実装に関しては、Androidエンジニアの方にリードしてもらいつつ、私はそれをiOSアプリへ導入する作業の一部を担当しました。

前提として、私はiOSアプリの開発経験が5年くらいあります。 Androidアプリの開発経験はほとんど無いので、Kotlinのコードを読むことにはあまり慣れていません。

XCFrameworkがローカルにない状態でiOSアプリをビルドするとエラーになる

knewではKMPのコードをXCFramework形式で出力し、iOS側で参照できるようにしました。 XCFramework形式での出力に関しては、以下のドキュメントに詳細が書かれています。

kotlinlang.org

CocoaPodsを使用してKMPのコードを参照する方法も用意されていましたが、knewではCocoaPodsからSwift Package Managerへの移行を進めている為、こちらの方法は選択しませんでした。

XCFrameworkの導入については、こちらの記事を参考に行いました。 gradlewコマンドを実行してKMPのコードをXCFrameworkとして生成し、それをiOS側で参照するような流れです。

記事に記載の通り、iOSアプリのBuild Phasesに以下のようなXCFrameworkを生成するスクリプトを追加しました。

しかし、ローカルファイルにXCFramworkが生成されていない状態でXcodeでビルドすると、以下のエラーになってしまいます。

There is no XCFramework found at '/Users/.../core/use_case/build/XCFrameworks/debug/use_case.xcframework'.

ターミナル等で一度gradlewコマンドを実行してXCframeworkを作成しておけばビルドは通るようになります。 しかし、環境構築等でローカルにXCFrameworkがない時に毎回この手順を行うのが面倒に感じたので、以下の方法で対応することにしました。

XCFrameworkがローカルにない状態でXcodeのビルドを行った場合には、XcodeのPre-actionsでビルドの直前にXCFrameworkを生成するようにしました。 Pre-actionsではローカルのXCFrameworkの有無をチェックしているので、生成済みの場合は処理をスキップさせています。

またBitriseでのiOSアプリのビルド時にも同様の問題が発生したので、ワークフローの中でビルド前にXCFrameworkの生成を行い、生成したXCFrameworkをキャッシュしておく対応を入れました。 キャッシュがある場合はPre-actionsの場合と同様に、処理をスキップするようにしました。

knewの場合、XCFrameworkを生成する処理に5〜10分程度掛かっていたので、キャッシュを利用して生成が不要な場合には処理をスキップしたことでビルドの時間を短縮することが出来ました!

XCFrameworkの名前に_が含まれていると、App Store Connectへのアップロードが失敗する

上記の対応を行ったことでビルドが通るようになり、iOSアプリからKMPの処理を呼び出せるようになりました。 ところが、実際にKMPのモジュールを含んだiOSアプリをApp Store Connectへアップロードしようとした際にエラーが発生しました。 以下の2種類のエラーが発生していました。

XCFrameworkのBundle Identifierに_が含まれているとinvalid charactersになる

knewではKMPのモジュールをいくつか作成していましたが、その中にuse_caseという名前のモジュールがあり、そのモジュール名がXCFrameworkのBundle Identifierに設定されていたことでエラーになっていました。 Bundle Identifierには_を付けることが出来ないようでした。

Asset validation failed (90049) This bundle is invalid. The bundle at path Payload/knew-app.app/Frameworks/use_case.framework has an invalid CFBundleIdentifier 'jp.co.livesense.core.use_case.use_case' There are invalid characters(characters that are not dots, hyphen and alphanumerics) that have been replaced with their code point 'jp.co.livesense.core.use\u005fcase.use\u005fcase' CFBundleIdentifier must be present, must contain only alphanumerics, dots, hyphens and must not end with a dot.

対応としては、use_caseモジュールのbuild.gradle.ktsでXCFrameworkの設定を記述している箇所で、binaryOptionの設定を入れてbundleIdを指定するようにしました。

 iosTargets.forEach {
        it.binaries.framework {
            baseName = "use_case"
            binaryOption("bundleId", "jp.co.livesense.core.useCase.useCase") // "_"が入らないようにキャメルケースの値を設定
            isStatic = true

            xcf.add(this)
        }
    }

binaryOptionについては以下のドキュメントを参考にしました。

kotlinlang.org

この設定を入れてXCFrameworkを生成することで、Bundle Identifierに_が入らない名称を指定することが出来ました。 use_caseというモジュール名自体を変更する方法もあったのですが、Androidのプロジェクトへの影響も大きかった為、よりプロジェクト全体への影響が少ないこちらの方法を選択しました。

XCFrameworkのminimum OS VersionがデフォルトでiOS 12になっている

XCFrameworkのminimum OS VersionがiOS 12に指定されていたことで以下のエラーになっていました。 knewは現在iOS 15以上をサポートしている為、minimum OS VersionにはiOS 15を指定する必要があります。

Asset validation failed (90208) Invalid Bundle. The bundle knew-app.app/Frameworks/data.framework does not support the minimum OS Version specified in the Info.plist.

XCFrameworkの生成時に最小OSバージョンに関する設定は特に入れていませんでした。 しかしXCFrameworkのinfo.plistを見てみると、何も指定しない場合はデフォルトでMinimumOSVersion12.0が設定されるようでした。

対応として、こちらの記事を参考に、それぞれのモジュールのbuild.gradle.ktsに最小OSバージョンの指定を入れるようにしました。何も指定を入れない場合はiOS 12が設定されていましたが、この設定を入れた状態でXCFrameworkを生成するとMinimumOSVersion15.0が設定されるようになります。

iosTargets.forEach {
        it.binaries.framework {
            baseName = "use_case"
            binaryOption("bundleId", "jp.co.livesense.core.useCase.useCase")
            freeCompilerArgs += listOf("-Xoverride-konan-properties=minVersionSinceXcode15.ios=15.0") // knewが現在サポートしている最小OSバージョンのiOS 15を指定
            isStatic = true

            xcf.add(this)
        }
    }

アプリのビルド自体は成功していたので、App Store Connectへのアップロードを行うまで気付くことが出来なかったのですが、 上記の対応を行うことで無事App Store Connectへアップロードも出来るようになりました!

おわりに

iOSアプリにKMPを導入した際にハマったことを書きました。

KMP導入をきっかけにAndroidのコードを読む機会が増えたので、少しだけKotlinのコードが分かるようになったり、 iOSアプリ開発との共通点や違いが分かったりと勉強になることが多くて楽しいです!

現時点でKMPが適用されている箇所はまだ限定的ですが、段階的にKMPへの移行を進めていきたいと考えています。 KMPの導入を進める中で得た知見は引き続き発信していきます。 最後まで読んでいただきありがとうございました!