Different Approaches in Consuming KMM Modules in iOS

Malvin Sutanto
Wantedly Engineering
7 min readFeb 19, 2021

Exploring various methods to work with KMM modules in an iOS project that is located in a separate repository.

With Kotlin Multiplatform for Mobile (KMM), you can share code between Android and iOS projects. When setting up a new KMM project, if you follow the KMM sample setup, you’ll have iOS, Android, and KMM shared code located in a single repository. However, when integrating KMM into existing apps, most likely your Android and iOS apps are located in separate repositories, with their own dedicated tooling and CI/CD pipeline.

Sharing KMM code with existing Android projects is pretty simple as you can easily upload your KMM into a Maven repository, but when it comes to iOS, it’s not that straightforward. In this article, I will show you different methods to distribute your KMM code so that it can be consumed in your iOS project.

This article is written based on Kotlin 1.4.20 and is not an exhaustive list of distribution methods for a KMM module. The Gradle build scripts in this article are all written in Kotlin.

CocoaPods Gradle plugin and git submodule

native.cocoapods is an official Gradle plugin from JetBrains that adds CocoaPods integration into a KMM project. To make use of this plugin, simply apply the native.cocoapods Gradle plugin in the build.gradle file of your KMM project and set up the necessary CocoaPods parameters. This plugin will add the necessary Gradle tasks to generate a podspec file for your KMM project. If you notice, the generated podspec file has a script_phase attribute that will automatically execute a Gradle task to compile the KMM module into a native framework when you run/build from XCode.

In this approach, you need to ensure that the generated .podspec file is pushed into the remote git repository, and then in your iOS repository, create a git submodule that points to the latest branch/release of your KMM project. Then, simply add the dependency to the podspec file in your iOS project’s Podfile.

target 'MyIosApp' do
// other dependencies.
pod 'MyKmmModule', :path => '/path/to/kmm/submodule'
end

There’s one more step to add before you can run pod install. Since the KMM module dependency is introduced as a vendored framework, CocoaPods needs to have access to the framework file during the pod install process to be able to configure the framework correctly. However, since the real framework file is only going to be generated when we run build in XCode, we need to run the following Gradle task before running the pod install command.

/path/to/kmm/submodule/gradlew generateDummyFramework

Now, you should be able to run pod install in your iOS project and then use the KMM shared code in XCode.

The current Gradle plugin only supports Debug and Release configuration for iOS projects. If you’re using custom configurations, you can add a custom Gradle task to add additional xcconfig settings and override the default configuration.

val podspec by tasks.existing(PodspecTask::class) {
doLast {
val outputFile = outputs.files.singleFile
val text = outputFile.readText()
val newText = text
// Workaround: https://youtrack.jetbrains.com/issue/KT-42023
.replace("spec.pod_target_xcconfig = {",
"""
spec.pod_target_xcconfig = {
'KOTLIN_CONFIGURATION' => 'Release',
'KOTLIN_CONFIGURATION[config=Debug]' => 'Debug',
""".trimIndent()
)
.replace("\$CONFIGURATION", "\$KOTLIN_CONFIGURATION")
outputFile.writeText(newText)
}
}

Pros

  • native.cocoapods is officially developed by JetBrains, and you can expect improvements in the future.
  • There’s little to no additional maintenance or work necessary to set up and share KMM code with iOS.

Cons

  • Your XCode build might be slow since it needs to download the Gradle dependencies and execute Gradle build, especially for a clean build.
  • Managing versioning with git submodule is not easy.

Generate and distribute universal (fat) frameworks

With the above CocoaPods and git submodule approach, XCode will execute a Gradle task to build the KMM module’s framework file whenever you run build in XCode. This task can be cached by Gradle, so if there are no changes in your KMM code, it should be pretty fast. But in a CI environment, you might have issues with slow build time.

Fortunately, as your KMM project is located in a separate repository, the KMM code itself should not change very often, hence, you can actually create a final native binary to be consumed in the iOS project. Moreover, since this can be delegated to the release pipeline of the KMM module’s CI/CD system, this would have a minimal impact on your XCode build.

You can create custom Gradle tasks in your Gradle build script to generate the universal (fat) frameworks for your KMM module:

kotlin {
val iosX64 = iosX64("iosX64") {
binaries {
framework {
baseName = "MyKmmModule"
embedBitcode("disable")
}
}
}
val iosArm64 = iosArm64("iosArm64") {
binaries {
framework {
baseName = "MyKmmModule"
embedBitcode("bitcode")
}
}
}
// Create a debug framework for both X64 and Arm64 architecture.
task.register<FatFrameworkTask>("debugFatFramework") {
baseName = "MyKmmModule"
destinationDir = file("$buildDir/fat-framework/debug")
from(
iosX64.binaries.getFramework("DEBUG"),
iosArm64.binaries.getFramework("DEBUG")
)
}
// Create a release framework for Arm64 architecture.
task.register<FatFrameworkTask>("releaseFatFramework") {
baseName = "MyKmmModule"
destinationDir = file("$buildDir/fat-framework/release")
from(
iosArm64.binaries.getFramework("RELEASE")
)
}
}

Once you have the framework files built, it’s up to you and your iOS team to decide on how to share and consume these framework files.

Pros

  • Building framework files can be delegated on the KMM module’s release pipeline in your CI/CD infrastructure, hence, there will be minimal impact on build time on your XCode build.
  • No dependency on Gradle and Maven for your iOS project.

Cons

  • You need to maintain the Gradle scripts for building these framework files.
  • Depending on how you decide to distribute the framework files, you might need additional work around the distribution pipeline and authentication around it.

Using transitive dependencies with Gradle

The other approach that you can use to consume KMM code from your iOS project is to create a separate Kotlin multiplatform project inside the iOS repository, add the dependencies to the KMM module using Gradle dependencies, and then expose that dependency using transitive dependencies. With this setup, managing the dependencies and version update should be much easier than using a git submodule approach. On top of it, we can also use native.cocoapods Gradle plugin to expose the KMM module to iOS.

To make sure that the headers for KMM modules are correctly exported, you need to enable transitiveDependency flag in the Gradle build script. Additionally, you can also include multiple KMM modules dependencies, effectively creating an umbrella framework for multiple KMM modules.

plugins {
kotlin("multiplatform") version "1.4.20"
kotlin("native.cocoapods") version "1.4.20"
}
kotlin {
ios {
// native.cocoapods Gradle plugin will declare
// the binary framework creation.
// Since, duplicate binary name is not supported,
// we need to configure the binary settings declared by
// native.cocoapods plugin to mark the dependency as an
// exported dependency.
binaries
.filterIsInstance<Framework>()
.forEach {
it.transitiveExport = true
it.export("com.my.kmm:module:1.0.0")
}
}
sourceSets {
getByName("iosMain") {
dependencies {
api("com.my.kmm:module:1.0.0")
}
}
}
cocoapods {
// Set up cocoapods parameters.
}
}

Similar to the git submodule approach, you need to run gradlew podspec task from the Kotlin multiplatform project to generate the latest podspec file. Then, add the dependency to the generatedpodspec file in your iOS project’s Podfile and run gradlew generateDummyFramework and pod install and you should now be able to invoke KMM functions inside your iOS project.

Pros

  • Allows you to import multiple KMM modules.
  • Dependencies are declared in a Gradle build file which is easier to manage.

Cons

  • You need to maintain a separate KMM project inside the iOS project.
  • Build time is similar to CocoaPods with git submodule approach because you still need to download and execute Gradle build for KMM modules.
  • Needs to ensure gradlew podspec is executed when a dependency is updated.

Swift Packages

Swift Package is a new format introduced by Apple to share reusable components between projects. There is a great article written by John O’ Reilly on how to use Swift Package Manager to consume KMM module in an iOS project. If you’re interested, you can find the article here: https://johnoreilly.dev/posts/kotlinmultiplatform-swift-package/

Summary

The approaches that I’ve shared above are by no means exhaustive. There are other methods that you can use to consume a KMM module from your iOS project. Also, the pros and cons of each approach that I’ve written might not apply to your project and team. It’s up to you to decide what works best for your configuration. However, I hope this can give you some ideas on some of the possibilities.

For us at Wantedly, we decided to use native.cocoapods Gradle plugin with Git submodule approach. Mainly because it’s one of the official Gradle plugins developed by JetBrains and we feel like the additional build time is a good trade-off for additional work necessary to create a custom solution to consume KMM module from our iOS project. Also, if you look at the result of the Kotlin Multiplatform survey published recently by JetBrains, there seem to be some improvements that we can expect in this area.

Thank you so much for reading this article, I hope you found it useful!

--

--