Skip to content

Instantly share code, notes, and snippets.

@Ribesg
Last active June 20, 2022 15:00
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ribesg/7a2b14841ad91d121bda24b6b1a7a054 to your computer and use it in GitHub Desktop.
Save Ribesg/7a2b14841ad91d121bda24b6b1a7a054 to your computer and use it in GitHub Desktop.
How to use a Kotlin MPP library depending on an iOS Framework in a XCode iOS App

This gist demonstrates how to use a Kotlin MPP library which depends on an iOS Framework in a native iOS application (XCode project).

First, see how the Kotlin MPP Library is built.

Key ideas:

  • We can currently only use a single Kotlin MPP library in an iOS project, so we need to create a MPP project local to our iOS XCode project whose sole purpose is to aggregate multiple MPP dependencies into a single Framework.
  • To keep our working environment clean, we create the gradle project in a subdirectory instead of at the root of our iOS XCode project. I name my subdirectory mpp.
  • We need to have the Frameworks used by our dependencies locally, as we only have bindings provided by the cinterop klibs, so we use Carthage again.
  • As the only goal of our gradle project is to create a Framework, we only use ios* targets.
  • Our project won't have any source code, but we NEED at least one source file, or Gradle will refuse to build anything, returning just a rude NO-SOURCE. Just create an empty Main.kt in src/iosMain/kotlin.
  • In addition to our empty source file, we need to export our dependencies using the export(...) function.
  • We need to build all required architectures then merge them into a single fat Framework using the lipo tool.
  • lipo only merges binaries, we need to have some common Framework metadata like the Info.plist, so we take the ones produced with the arm64 Framework. The only problem is that the Info.plist of that Framework states that it is arm64, so we need to fix this with the PlistBuddy tool.
  • Frameworks don't really need a version in the iOS world (at least when using Carthage), so our project doesn't have one, but if you want you can set one in the same PlistBuddy task.
  • We build the Framework using the buildFramework task. We then just import this Framework in XCode normally, it's location is mpp/build/ios/releaseFramework/Mpp.framework. You can add ./gradlew buildFramework as a build step in your XCode project if you want.
  • Once the Framework is imported into XCode, just use import Mpp at the top of any Swift file to be able to use any of the dependencies of your local Gradle MPP project.
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTargetPreset
repositories {
google()
jcenter()
mavenLocal()
}
plugins {
id("org.jetbrains.kotlin.multiplatform") version "1.3.31"
}
data class IosTarget(val name: String, val preset: String, val archName: String, val archId: String)
val iosTargets = listOf(
IosTarget("ios", "iosArm64", "arm64", "arm64"),
IosTarget("iosSim", "iosX64", "x64", "x86_64")
)
// Here you can set the name of the Framework you will import in XCode and in your Swift files
val frameworkName = "Mpp"
kotlin {
for (iosTarget in iosTargets) {
targetFromPreset(presets.getByName<KotlinNativeTargetPreset>(iosTarget.preset), iosTarget.name) {
binaries {
framework(listOf(RELEASE)) {
baseName = frameworkName
debuggable = true
export("com.example.lib:my-mpp-library-ios-${iosTarget.archName}:1.2.3")
linkerOpts("-F$projectDir/Carthage/Build/iOS")
}
}
}
}
sourceSets {
getByName("commonMain") {
dependencies {
implementation(kotlin("stdlib-common"))
api("com.example.lib:my-mpp-library:1.2.3")
}
}
}
}
// Carthage tasks
listOf("bootstrap", "update").forEach { type ->
task<Exec>("carthage${type.capitalize()}") {
group = "carthage"
executable = "carthage"
args(
type,
"--platform", "iOS",
"--no-use-binaries", // Provided binaries are sometimes problematic, remove this to speedup process
"--cache-builds"
)
}
}
tasks.withType<KotlinNativeCompile>().all {
dependsOn("carthageBootstrap")
}
// Lipo tasks
data class LipoParams(val type: String, val bundle: String, val binary: String)
val lipoParams = arrayOf(
LipoParams("Framework", "$frameworkName.framework", libraryName),
LipoParams("FrameworkDsym", "$frameworkName.framework.dSYM", "Contents/Resources/DWARF/$frameworkName")
)
for (params in lipoParams) {
tasks.create<Exec>("lipoIos${params.type}") {
group = "lipo"
for (iosTarget in iosTargets) {
dependsOn("compileKotlin${iosTarget.name.capitalize()}")
dependsOn("linkReleaseFramework${iosTarget.name.capitalize()}")
}
val bundlePaths = mutableMapOf<String, String>()
val binaryPaths = mutableMapOf<String, String>()
for (iosTarget in iosTargets) {
bundlePaths[iosTarget.name] = "$buildDir/bin/${iosTarget.name}/releaseFramework/${params.bundle}"
binaryPaths[iosTarget.name] = "${bundlePaths[iosTarget.name]}/${params.binary}"
}
val resBundlePath = "$buildDir/ios/releaseFramework/${params.bundle}"
val resBinaryPath = "$resBundlePath/${params.binary}"
doFirst {
mkdir(File(resBinaryPath).parent)
}
executable = "lipo"
val args = mutableListOf<String>()
args.add("-create")
for (iosTarget in iosTargets) {
args.add("-arch")
args.add(iosTarget.archId)
args.add(binaryPaths[iosTarget.name]!!)
}
args.add("-output")
args.add(resBinaryPath)
args(*args.toTypedArray())
doLast {
// Copy arm64 framework metadata into fat framework
copy {
from(bundlePaths[iosTargets.first().name]!!) {
exclude(params.binary)
}
into(resBundlePath)
}
// Tweak Framework's Info.plist
val frameworkPlistPath = "$resBundlePath/Info.plist"
if (File(frameworkPlistPath).exists()) {
exec {
executable = "/usr/libexec/PlistBuddy"
args(
"-c", "Delete :UIRequiredDeviceCapabilities",
frameworkPlistPath
)
}
}
}
}
}
// Main Framework build task
tasks.create("buildFramework") {
group = "build"
dependsOn(tasks.filter { it.group == "lipo" })
tasks.getByName("build").dependsOn(this)
}
tasks.named<Delete>("clean") {
delete(buildDir)
}
github "bugsnag/bugsnag-cocoa" ~> 5.0
// This project is not meant to be published anyway, so it's just the name of the IDEA project
rootProject.name = "mpp"
pluginManagement {
repositories {
google()
gradlePluginPortal()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment