KMM — Publish iOS/ watchOS / tvOS SDK Binaries

Rakesh Chander
4 min readFeb 13, 2022

KMM has been getting attention of developers and it , obviously, deserves that.

We have different dependency managers for iOS , and KMM binaries are required to be made available on same for easy integration process of KMM Library consumers.

In this article — I am going to explain the steps needed for publishing KMM libraries via

  • CocoaPods
  • SPM — Swift Package Manager
  • Carthage

I have opted for below formats of SDKs — to be shared

  • XCFramework — iOS/WatchOS/TvOS

Lets start with supporting files setup

CocoaPods — Podspec file SetUp — Create SDK_NAME.podspec at project root with below contents

Pod::Spec.new do |spec|
spec.name = '<SDK_NAME>'
spec.version = "1.0.0"
spec.homepage = '<GIT_REPO_URL>'
spec.source = { :http => "<GIT_REPO_URL>/releases/download/1.1.0/RCCachingManager.xcframework.zip"}
spec.authors = '<EMAIL_ID>'
spec.license = 'https://opensource.org/licenses/Apache-2.0'
spec.summary = '<DESCRIPTION>'

spec.vendored_frameworks = "<SDK_NAME>.xcframework"

spec.ios.deployment_target = '12.0'
spec.watchos.deployment_target = '7.0'
spec.tvos.deployment_target = '13.0'


spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=*simulator*]' => 'arm64' }
spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=*simulator*]' => 'arm64' }

end

Register your machine & email as publisher on CocoaPods

pod trunk register <GIT_EMAIL_HAVING_ACCESS_TO_REPO> '<NAME>' --description='<DESCRIPTION>

SPM — Package.swift file SetUp — Create Package.swift at project root with below contents

// swift-tools-version:5.4
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "<SDK_NAME>",
platforms: [
.iOS(.v12), .tvOS(.v13), .watchOS(.v7)
],
products: [
.library(
name: "<SDK_NAME>",
targets: ["<SDK_NAME>"]),
],
dependencies: [
// no dependencies
],
targets: [
.binaryTarget(
name: "<SDK_NAME>",
url: "<GIT_REPO_URL>/releases/download/1.0.0/<SDK_NAME>.xcframework.zip",
checksum: "c8982f3e5e56adea5355396a68fb09c37f9223d09738ed1037c643b590e38573"
),
]
)

Carthage — JSON file SetUp — Create Folder with Carthage name — having file <SDK_NAME>.json — at project root with below contents

{
"1.0.0":"<GIT_REPO_URL>/releases/download/1.0.0/<SDK_NAME>.xcframework.zip"
}

Lets get started for build.gradle.kts setup

import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

Create a global variable

version = "1.1.0"
val iOSBinaryName
= "<SDK_NAME>"

Under kotlin section, add below

val xcf = XCFramework()
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it
.binaries.framework {
baseName
= "${iOSBinaryName}"
xcf.add(this)
}
}

watchos {
binaries
.framework {
baseName
= "${iOSBinaryName}"
xcf.add(this)
}
}

tvos {
binaries
.framework {
baseName
= "${iOSBinaryName}"
xcf.add(this)
}
}

Under sourceSets sub-section of kotlin section, add below —

val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val watchosMain by getting
val tvosMain by getting

val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
watchosMain.dependsOn(this)
tvosMain.dependsOn(this)

}
val
iosX64Test by getting
val iosArm64Test by getting
val iosSimulatorArm64Test by getting
val iosTest by creating {
dependsOn(commonTest)
iosX64Test.dependsOn(this)
iosArm64Test.dependsOn(this)
iosSimulatorArm64Test.dependsOn(this)
}

Add below in build.gradle.kts — module level

/**
* This task prepares Release Artefact for iOS XCFramework
* Updates Podspec, Package.swift, Carthage contents with version and checksum
*/
tasks
.register("prepareReleaseOfiOSXCFramework") {
description = "Publish iOS framework to the Cocoa Repo"

// Create Release Framework for Xcode
dependsOn("assembleXCFramework", "packageDistribution")

doLast {

// Update Podspec Version
val poddir = File("$rootDir/$iOSBinaryName.podspec")
val podtempFile = File("$rootDir/$iOSBinaryName.podspec.new")

val podreader = poddir.bufferedReader()
val podwriter = podtempFile.bufferedWriter()
var podcurrentLine: String?

while (podreader.readLine().also { currLine -> podcurrentLine = currLine } != null) {
if (podcurrentLine?.trim()?.startsWith("spec.version") == true) {
podwriter.write(" spec.version = \"${version}\"" + System.lineSeparator())
} else if (podcurrentLine?.trim()?.startsWith("spec.source") == true) {
podwriter.write(" spec.source = { :http => \"https://github.com/rakeshchander/CachingLibrary-KMM/releases/download/${version}/${iOSBinaryName}.xcframework.zip\"}" + System.lineSeparator())
} else {
podwriter.write(podcurrentLine + System.lineSeparator())
}
}
podwriter.close()
podreader.close()
podtempFile.renameTo(poddir)

// Update Cartfile Version
val cartdir = File("$rootDir/Carthage/$iOSBinaryName.json")
val carttempFile = File("$rootDir/Carthage/$iOSBinaryName.json.new")

val cartreader = cartdir.bufferedReader()
val cartwriter = carttempFile.bufferedWriter()
var cartcurrentLine: String?

while (cartreader.readLine().also { currLine -> cartcurrentLine = currLine } != null) {
if (cartcurrentLine?.trim()?.startsWith("{") == true) {
cartwriter.write("{"+ System.lineSeparator())
cartwriter.write(" \"${version}\":\"https://github.com/rakeshchander/CachingLibrary-KMM/releases/download/${version}/${iOSBinaryName}.xcframework.zip\","+ System.lineSeparator())
} else if (cartcurrentLine?.trim()?.startsWith("\"${version}\"") == true) {
continue
} else {
cartwriter.write(cartcurrentLine + System.lineSeparator())
}
}
cartwriter.close()
cartreader.close()
carttempFile.renameTo(cartdir)

// Update Package.swift Version

// Calculate Checksum
val checksumValue: String = org.apache.commons.io.output.ByteArrayOutputStream()
.use { outputStream ->
// Calculate checksum
project
.exec {
workingDir = File("$rootDir")
commandLine("swift", "package", "compute-checksum", "${iOSBinaryName}.xcframework.zip")
standardOutput = outputStream
}

outputStream.toString()
}



val
spmdir = File("$rootDir/Package.swift")
val spmtempFile = File("$rootDir/Package.swift.new")

val spmreader = spmdir.bufferedReader()
val spmwriter = spmtempFile.bufferedWriter()
var spmcurrentLine: String?

while (spmreader.readLine().also { currLine -> spmcurrentLine = currLine } != null) {
if (spmcurrentLine?.trim()?.startsWith("url") == true) {
spmwriter.write(" url: \"<GIT_REPO_URL>/releases/download/${version}/${iOSBinaryName}.xcframework.zip\"," + System.lineSeparator())
} else if (spmcurrentLine?.trim()?.startsWith("checksum") == true) {
spmwriter.write(" checksum: \"${checksumValue.trim()}\"" + System.lineSeparator())
} else {
spmwriter.write(spmcurrentLine + System.lineSeparator())
}
}
spmwriter.close()
spmreader.close()
spmtempFile.renameTo(spmdir)
}
}
**
* Task to create zip for XCFramework
* To be used by Carthage for Distribution as Binary
*/
tasks
.create<Zip>("packageDistribution") {
// Delete existing XCFramework
delete("$rootDir/XCFramework")

// Replace XCFramework File at root from Build Directory
copy {
from("$buildDir/XCFrameworks/release")
into("$rootDir/XCFramework")
}

// Delete existing ZIP, if any
delete("$rootDir/${iOSBinaryName}.xcframework.zip")
// ZIP File Name - as per Carthage Nomenclature
archiveFileName
.set("${iOSBinaryName}.xcframework.zip")
// Destination for ZIP File
destinationDirectory
.set(layout.projectDirectory.dir("../"))
// Source Directory for ZIP
from(layout.projectDirectory.dir("../XCFramework"))
}

Distribution SetUp

Sync your gradle file. You will find below task added to your Project

  • prepareReleaseOfiOSXCFramework

You can execute this command now via Terminal OR double click from task list.

Save <SDK_NAME>.xcframework.zip (from project root) to some other location.

Push your Code and Merge to master.

Create a GIT Release with same version as that of build.gradle.kts and upload <SDK_NAME>.xcframework.zip as release artefacts.

For CocoaPods Publish, execute below command-

pod trunk push <SDK_NAME>.podspec --allow-warnings

Now you can import in your iOS/ watchOS / tvOS projects following below —

CocoaPods

  • Add below in podfile — in respective target block
source 'https://github.com/CocoaPods/Specs.git'pod '<SDK_NAME>'

Swift Package Manager — SPM

Once you have your Swift package set up, adding SDK as a dependency is as easy as adding it to the dependencies value of your Package.swift.

dependencies: [
.package(url: "<GIT_REPO_URL>")
]

Carthage (min Carthage version 0.38.0)

To integrate SDK into your Xcode project using Carthage, specify it in your Cartfile:

binary "<GIT_REPO_URL>/blob/main/Carthage/GrowthBook.json"

Execute below command -

source 'https://github.com/CocoaPods/Specs.git'

All Set!! Distribute SDKs

--

--

Rakesh Chander

I believe in modular development practices for better reusability, coding practices, robustness & scaling — inline with automation.