DEV Community

Cover image for Enhance iOS Development with Kotlin Multiplatform Library
Arseni Kavalchuk
Arseni Kavalchuk

Posted on

Enhance iOS Development with Kotlin Multiplatform Library

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 your build.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()
    }
}
Enter fullscreen mode Exit fullscreen mode

Run the Gradle task to build the library, producing an XCFramework for iOS integration.

Application Architecture and Business Logic

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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:

  1. Set the Framework Name: Ensure the framework has a clear and unique name, like KmpLib.
  2. Specify Target Platforms: Define architectures such as iosArm64 for physical devices and iosSimulatorArm64 for simulators.
  3. 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 the kmp-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:

  1. Input sample data into the app.
  2. Encrypt and store the data using Kotlin’s business logic.
  3. Retrieve and decrypt the data, showcasing the seamless interplay between Swift and KMP.

Application in Xcode

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:

  1. 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.
  2. Click on a project to open a Project Settings.
  3. Under Frameworks, Libraries, and Embedded Content, click the + button.
  4. 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.

Xcode settings

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:

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)")
                }
            })
Enter fullscreen mode Exit fullscreen mode

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:

Sample Application


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)