Blog Infos
Author
Published
Topics
, , , ,
Published
Gemini generated

Kotlin/Native provides indirect interoperability with Swift through Objective-C.

This is what the documentation says.

While developing my Kotlin Multiplatform library — HealthKMP — I thought after assembling and publishing it would be ready to use in Swift right away. I was wrong.

Problem 1: kotlin.Result

kotlin.Result is an inline class which is not supported in Swift/Objective-C, and as a result we get Any? here.

Inlined kotlin.Result

 

interface HealthManager {

    fun isAvailable(): Result<Boolean>

}

As a possible solution we can create our own wrapper on top of kotlin.Result:

sealed interface KmpResult<out T> {

    data class Success<T>(val value: T) : KmpResult<T>

    data class Failure(val error: Throwable) : KmpResult<Nothing>
}

inline fun <T> kmpResult(block: () -> T): KmpResult<T> {
    return try {
        KmpResult.Success(block())
    } catch (e: Throwable) {
        KmpResult.Failure(e)
    }
}

interface HealthManager {

    fun isAvailable(): KmpResult<Boolean>

}

And in Swift we would get:

KmpResult as return type

 

let result = health.isAvailable()

switch(result) {
case let success as KmpResultSuccess<KotlinBoolean>:
    let isAuthorized : KotlinBoolean? = success.value
    print("Success \(isAuthorized ?? false)")
case let failure as KmpResultFailure:
    print("Failure \(failure.error.description())")
default:
    print("Unknown result")
}

 

But even if we specify that method returns not nullable result, we still get nullable value in success case.

I prefer another solution. Let’s create a wrapper for iosMain / appleMain targets:

interface HealthManager {

    fun isAvailable(): Result<Boolean>

}

class SwiftHealthManager(
    private val manager: HealthManager,
) {

    @Throws(Throwable::class)
    fun isAvailable(): Boolean {
        return manager.isAvailable().getOrThrow()
    }

}

And in Swift we would have the correct return type:

Inlined return type

 

 

do {
    let isAvailable : Bool = try health.isAvailable()
    print("Success \(isAvailable)")
} catch {
  print("Failure \(error)")
}

 

Problem 2: kotlinx.datetime

I am using kotlinx.datetime.Instant to specify a date range. But when it comes to creating its instance in Swift, it doesn’t look like I expected it to be.

I want to continue using Date in Swift instead of managing a new type Kotlinx_datetimeInstant.

We can extend the existing wrapper in iMain / appleMain targets to accept NSDate:

class SwiftHealthManager(
    private val manager: HealthManager,
) {

    @Throws(Throwable::class)
    suspend fun readData(
        startTime: NSDate,
        endTime: NSDate,
        type: HealthDataType,
    ): List<HealthRecord> {
        return manager.readData(
            startTime = startTime.toKotlinInstant(),
            endTime = endTime.toKotlinInstant(),
            type = type,
        ).getOrThrow()
    }

}

And now we could create Date in Swift as usual:

do {
    let records : [HealthRecord] = try await health.readData(
        startTime: Calendar.current.date(byAdding: .day, value: -7, to: Date())!,
        endTime: Date.now,
        type: HealthDataTypeWeight(),
    )
    print("Success \(records.count)")
} catch {
    print("Failure \(error.localizedDescription)")
}
Problem 3: Extensions

Kotlin extensions are exported in Swift, but in order to access them we need to know the file name, they don’t behave as usual Swift extensions.

And this is where SKIE comes to help.

SKIE is a tool for Kotlin Multiplatform development that enhances the Swift API published from Kotlin.

Intallation
  1. libs.versions.toml:

 

[versions]
skie = "0.10.2"

[libraries]

[plugins]
skie = { id = "co.touchlab.skie", version.ref = "skie" }

 

2. project build.gradle.kts:

plugins {
    alias(libs.plugins.skie) apply false
}

3. KMP module build.gradle.kts:

 

plugins {
    // other plugins
    alias(libs.plugins.skie)
}

 

And that’s it, after assembling we will get Swift friendly code.

Skie result

Now we can access Kotlin extensions in Swift:

Swift extension itself:

extension Date {

    public func toKotlinInstant() -> Kotlinx_datetimeInstant
}

The original Kotlin extension:

fun NSDate.toKotlinInstant(): Instant = toKotlinInstant()

Skie is not only for extensions, see here for supported Kotlin features.

 

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Sharing code across platforms is a wonderful superpower. But sometimes, sharing 100% of your codebase isn’t the goal. Maybe you’re migrating existing apps to multiplatform, maybe you have platform-specific libraries or APIs you want to…
Watch Video

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Developer

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform ...

When sharing isn’t caring: Using platform-specific code in Kotlin Multiplatform

Russell Wolf
Kotlin Multiplatform Deve ...

Jobs

Final thoughts

Building multiplatform library using Kotlin becomes easy nowadays, we can have shareed common Kotlin code and platform specific code for each target, publish our library to maven central Kotlin Multiplatform library or as Swift library to Swift Package Manager.

Check out HealthKMP — Kotlin Multiplatform library for Apple HealthKit on iOS / watchOS and Google Fit / Health Connect on Android.

This article was previously published on proandroiddev.com.

Menu