Kotlin Multiplatform (KMP) is a promising approach that enables developers to share business logic across multiple platforms, including iOS and Android, while allowing platform-specific implementations for platform-dependent functionalities. This article demonstrates how to integrate KMP with iOS, showcasing how to expose Kotlin interfaces to Swift, implement these interfaces in Swift, and leverage them in a Swift UI application.
The focus here is to explore an alternative approach that avoids the complexity of expect/actual
patterns. Instead, this method streamlines the integration by minimizing the interaction with platform APIs not exposed in KMP.
We’ll build a sample Swift UI application that encrypts, stores, and decrypts data. The business logic resides in Kotlin, while platform-specific functionality is implemented in Swift.
Setting Up the Kotlin Mutliplatform Library in Android Studio
To begin, you’ll need to create a KMP library using Android Studio. For reference, you can use the example project. The build.gradle.kts
file for the library includes important configurations for creating a multiplatform library.
Key Steps:
- Use basic KMP project structure. See example project for the reference
- Define a list of iOS targets
iosX64
,iosArm64
,iosSimulatorArm64
in yourbuild.gradle.kts
. - Define
XCFramework
with a variable and add all targets to the framework - The key point here is that you need to give some meaningful name to your framework as well as using the same name for the framework binary.
The minimal build.gradle.kts
file looks like this:
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
group = "io.github.arskov"
version = "1.0.0"
// Use some name for the framework. It will be used as main import in Swift
val iosXCFrameworkName = "KmpLib"
val binaryName = "kmp-lib"
kotlin {
androidTarget {
publishLibraryVariants("release")
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_1_8)
}
}
val xcf = XCFramework(iosXCFrameworkName)
val iosTargets = listOf(iosX64(), iosArm64(), iosSimulatorArm64())
iosTargets.forEach {
it.binaries.framework {
baseName = iosXCFrameworkName
xcf.add(this)
}
}
sourceSets {
commonMain {
dependencies {
// Put necessary KMP compatible dependencies here
}
}
commonTest {
dependencies {
implementation(libs.kotlin.test)
}
}
}
}
android {
namespace = "io.github.arskov"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
}
Run the Gradle task to build the library, producing an XCFramework for iOS integration.
Application Architecture and Business Logic
- Kotlin module contains MultiplatformService which accepts 2 interfaces as dependencies: CryptoProvider and StoreProvider
package io.github.arskov
class MultiplatformService(
private val cryptoProvider: CryptoProvider,
private val storeProvider: StoreProvider
) {
@Throws(StoreException::class, EncryptionException::class)
fun storeData(data: PlainData, encryptionKey: ByteArray?) {
if (!data.encrypted && encryptionKey != null) {
val encryptedData =
cryptoProvider.encrypt(op = EncryptOp.ENCRYPT, key = encryptionKey, data = data.content)
val encryptedPlainData =
PlainData(id = data.id, encrypted = true, updated = 0L, content = encryptedData)
storeProvider.store(encryptedPlainData)
} else {
storeProvider.store(data)
}
}
@Throws(StoreException::class, EncryptionException::class)
fun loadData(id: Long, encryptionKey: ByteArray?): PlainData {
val data = storeProvider.load(id)
if (data.encrypted && encryptionKey != null) {
val decryptedData = cryptoProvider.encrypt(op = EncryptOp.DECRYPT, key = encryptionKey, data = data.content)
return PlainData(id = data.id, encrypted = false, updated = 0L, content = decryptedData)
}
return data
}
}
The main idea of the library is accepting the data, perform a provided crypto operation, and store the value in the provided store.
For simplicity, we also have a default in-memory implementation of the StoreProvider
, but this could also be implemented on the Swift side. See below.
Now what we want to do is to integrate this MultiplatformService
into Swift UI sample application. In Swift when we construct the MultiplatformService
we provide a Swift implementation of the CryptoProvider
and the StoreProvider
.
Building the XCFramework
An XCFramework is a container that supports multiple architectures, allowing your library to work seamlessly on different iOS devices and simulators.
Steps to Build the XCFramework:
-
Set the Framework Name: Ensure the framework has a clear and unique name, like
KmpLib
. -
Specify Target Platforms: Define architectures such as
iosArm64
for physical devices andiosSimulatorArm64
for simulators. -
Run the Build Task:
Use
./gradlew task
to see what are the possible assemble tasks you have. Use the Gradle task./gradlew assembleKmpLibXCFramework
to build the XCFramework. The output will be located in thekmp-lib/library/build/XCFrameworks/{debug|release}/
directory. Gradle generates assembleXCFramework task if you give your framework a name.
Sample Swift UI Application
The Swift UI application demonstrates how to utilize the KMP library for secure data management. Here's a high-level overview of its functionality:
-
Encryption: Data is encrypted using the
CryptoProvider
interface implemented in Swift. -
Storage: Encrypted data is stored using the
InMemoryStoreProvider
or a custom implementation. - Decryption: The app retrieves and decrypts the data for display.
Example Workflow:
- Input sample data into the app.
- Encrypt and store the data using Kotlin’s business logic.
- Retrieve and decrypt the data, showcasing the seamless interplay between Swift and KMP.
The app architecture is minimalist yet demonstrates how Kotlin Multiplatform libraries can enhance iOS development.
Importing the XCFramework into Xcode
To use the generated XCFramework in your Xcode project, follow these steps:
- Create a new iOS Swift UI project using the Xcode wizard. You don't need any special steps for this, just follow the Xcode instructions.
- Click on a project to open a Project Settings.
- Under Frameworks, Libraries, and Embedded Content, click the + button.
- Add the path to the XCFramework:
kmp-lib/library/build/XCFrameworks/debug/KmpLib.xcframework
. The framework will appear in the Frameworks section of the Project structure in the left tree panel.
This integration allows Swift to access Kotlin’s business logic.
Implementing KMP Interfaces in Swift
In our example, we’ve imported the KmpLib
framework and now we are going to implement its exposed interfaces in Swift. The key interfaces are:
- CryptoProvider: Handles encryption and decryption of data.
-
StoreProvider: Provides storage functionality. In this case, we use the default
InMemoryStoreProvider
from KMP. - Example of the
IosCryptoProvider
in Swift that implements theCryptoProvider
interface (a protocol in Swift)
import KmpLib
class IosCryptoProvider: CryptoProvider {
func encrypt(op: EncryptOp, key: KotlinByteArray, data: KotlinByteArray)
throws -> KotlinByteArray
{
// Ensure the key is not empty
guard key.size > 0 else {
throw NSError(
domain: "CryptoError", code: 1,
userInfo: [NSLocalizedDescriptionKey: "Key must not be empty"])
}
// Create a result array of the same size as the data
let result = KotlinByteArray(size: data.size)
// Perform XOR encryption
for i in 0..<data.size {
let dataByte = data.get(index: i)
let keyByte = key.get(index: i % key.size)
result.set(index: i, value: dataByte ^ keyByte)
}
return result
}
}
The ServiceLocator which contains the main code that instantiates and crates a Singleton for MultiplatformService
:
import Foundation
import KmpLib
class ServiceLocator {
static let sharedInstance = ServiceLocator()
private let mutliplatformService: MultiplatformService!
private let storeProvider: StoreProvider!
private init() {
// Swift implementation of the CryptoProvider protocol exposed from KMP
let cryptoProvider = IosCryptoProvider()
// KMP implementation of Stor
storeProvider = InMemoryStoreProvider()
mutliplatformService = MultiplatformService(
cryptoProvider: cryptoProvider, storeProvider: storeProvider)
}
func getMultiplatformService() -> MultiplatformService {
return self.mutliplatformService
}
func getStoreProvider() -> StoreProvider {
return self.storeProvider
}
}
The ContentView.swift file contains main UI logic:
- Set the password for the content encryption
- Set the text
- Button to encrypt and store the content
- Controls to display the encrypted text in HEX
- Button to load and decrypt the content
Button(action: {
let service = ServiceLocator.sharedInstance
.getMultiplatformService()
let store = ServiceLocator.sharedInstance.getStoreProvider()
let plainData = PlainData(
id: 1,
encrypted: false,
updated: 0,
content: self.plainDataText.toKotlinByteArray()
)
do {
try service.storeData(
data: plainData,
encryptionKey: encryptionKey.toKotlinByteArray())
let storedData = try store.load(id: 1)
self.encryptedDataText = storedData.content.toHexString()
} catch {
print("error: \(error)")
}
})
Important detail, that we also crated some extension functions for KotlinByteArray
and String
to be able to easily convert between them.
In production this copy operation is not cheap, and especially with the default methods that the KMP exposes for KotlinByteArray
, like get(i)
, set(i, value)
. We will consider alternatives in the follow up articles. Stay tuned.
The running application looks like:
Conclusion
This article provided a step-by-step guide on leveraging Kotlin Multiplatform libraries for iOS development. By following this alternative approach, you can efficiently share business logic across platforms without delving into the complexities of expect/actual
APIs. The sample Swift UI application highlights the practical integration of KMP interfaces in Swift, showcasing the potential for streamlined cross-platform development.
For further exploration, check out the complete example on GitHub.
Top comments (0)