Hi, can SKIE handle Kotlin Result<T> type in...
# touchlab-tools
j
Hi, can SKIE handle Kotlin Result<T> type in a way to handle it on Swift side like this:
someResult.onSuccess { }.onFailure { }
I tried SKIE, but wasn't able to achieve that. If thats't not possible any recommendations regarding the return types?
t
Not at this moment, but could you create a discussion at https://github.com/touchlab/SKIE/discussions so we can keep track of this feature request? Thank you! One thing that's available to you now is writing your own Swift code using SKIE's Swift bundling to add the
onSuccess {}.onFailure {}
DSL. That way your iOS folks will be able to use it.
🙏 1
j
do you have any resources how to do that? I checked that page but maybe there is some samples with actual code? https://skie.touchlab.co/features/swift-code-bundling
t
I'm not sure, but it's just putting any Swift code to
src/iosMain/swift
(or even
src/commonMain/swift
) and you have access to all the exported Kotlin types. The one thing to keep in mind is that default Swift visibility is
internal
, so you need to make stuff
public
if it should be visible from outside of the Kotlin framework.
j
Thanks, will try
I tried it but TestResource isn't available in iOS project
I'm testing it by running
./gradlew spmDevBuild -PspmBuildTargets=ios_simulator_arm64
and then copy paste generated framework inside xcode project
t
Can you show the contents of the swift file?
j
Copy code
import Foundation

public struct TestResource {
    func test() {
        print("I'm resource")
    }
}
Somehow generated files from SKIE had
TestResource
but now I can't test it cause
spmDevBuild
fails with noswiftintefaces found error
r
We have a similar approach in our KMP project, but we choose not use the standard Kotlin Result class, but to use our own so we have more control as the standard Result class looses type information in Objective-C.
Copy code
sealed class KmpResult<out T : Any> {
    class Success<out T : Any>(val data: T) : KmpResult<T>()
    class Error(val error: KmpException) : KmpResult<Nothing>()

..........
    fun onSuccess(action: (T) -> Unit): KmpResult<T> {
        if (this is Success<T>) action(this.data)
        return this
    }

    fun onError(action: (KmpException) -> Unit): KmpResult<T> {
        if (this is Error) action(this.error)

        return this
    }

......
}
In our iOS project we can then call this like your original question
Copy code
result
  .onError { error in  }
  .onSuccess {data in  }
j
onSuccess {data in  }
is type of
data
available at compile time?
r
Yep! 😄
🎉 1
j
Great ^^
t
@Jemo "fails with noswiftintefaces found error" Make sure you're using the latest SKIE version. If you do, there might be something messing up SKIE's automatic output detection. You can manually override it with:
Copy code
skie {
    build {
        enableSwiftLibraryEvolution.set(true)
    }
}
Lastly I'd caution against using SPM as it usually leads to longer compilation times (especially if you're using XCFramework).
j
Wow,
enableSwiftLibraryEvolution.set(true)
fixed all issues, swift files are also included in final XCFramework. Thanks a lot 🎉
P.S I'm using "0.8.2" version
t
Could you share what plugin you use for the spmDevBuild task?
j
KMMBridge
i
There’s a issue - when you found this from your swift code site
For-in loop requires 'any Kotlinx_coroutines_coreFlow' to conform to 'AsyncSequence'
just add implementation(“co.touchlab.skieruntime kotlin${libs.versions.touchlab.skie.get()}“) to fix that.
t
No, don’t, that doesn’t seem right
Does your module have dependency on kotlinx coroutines core?
i
Yes. i have that. it’s no issue with
Copy code
kotlin 1.9.22
kotlinx-coroutines 1.8.1
touchlab-skie 0.6.1
t
If you do, only think of it as a workaround, we need to find a reproducer and fix it
Try SKIE 0.8.4
i
it’s coming after upgrade to touchlab-skie 0.8.2
let me check if 0.8.4 works otherwise let me try create a reproducer
t
Sorry, it’s 0.8.2
I always forget
Interesting, I’ll try to reproduce it tomorrow
We changed it to only add the runtime if you depend on coroutines, but there’s probably a bug in how it’s being determined
🫡 1
i
Copy code
import co.touchlab.skie.configuration.DefaultArgumentInterop

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.kotlinx.serialization)
    alias(libs.plugins.kover)
    alias(libs.plugins.touchlab.skie)
    alias(libs.plugins.cash.sqldelight)
    alias(libs.plugins.touchlab.kmmbridge)
    `maven-publish`
}

/**
 * will update to for createSwiftPackage as we
 */
version = "1.0-SNAPSHOT"

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

kotlin {
    jvm()
    listOf(
//        macosX64(),
        macosArm64(),
    ).forEach { iosTarget ->
        iosTarget.binaries.framework {
            export(libs.androidx.lifecycle.viewmodel)
            export(libs.koin.core)
            isStatic = true
            export(libs.napier)
        }
    }

    sourceSets {
        commonMain.dependencies {
            // put your Multiplatform dependencies here
            implementation(libs.kotlinx.serialization.json)
            implementation(libs.bundles.ktor.client)
            implementation(libs.kotlinx.coroutines.core)
            implementation(libs.tddworks.openai.client.core)

            // view model
            api(libs.androidx.lifecycle.viewmodel)

            // Koin
            api(libs.koin.core)
            implementation(libs.koin.composeVM)

            // SQLDelight
            //<https://hyperskill.org/learn/step/33432#install-and-configure-sqldelight-async-extension>
            implementation(libs.app.cash.sqldelight.async.extensions)
            implementation(libs.app.cash.sqldelight.coroutines.extensions)

            // logging
            api(libs.napier)

            // testing
            implementation(libs.koin.test)
        }

        commonTest.dependencies {
            implementation(libs.ktor.client.mock)
        }

        appleMain.dependencies {
            implementation(libs.app.cash.sqldelight.native.driver)
            implementation(libs.tddworks.openai.client.darwin)
//            implementation("co.touchlab.skie:runtime-kotlin:${libs.versions.touchlab.skie.get()}")
        }

        jvmMain.dependencies {
            implementation(libs.ktor.client.cio)
            implementation(libs.app.cash.sqldelight.sqlite.driver)
        }

        jvmTest.dependencies {
            implementation(project.dependencies.platform(libs.junit.bom))
            implementation(libs.bundles.jvm.test)
            implementation(libs.kotlinx.coroutines.test)
            implementation(libs.app.cash.turbine)
            implementation(libs.koin.test.junit5)
        }
    }
}

addGithubPackagesRepository() // <- Add the GitHub Packages repo

kmmbridge {
    /**
     * reference: <https://kmmbridge.touchlab.co/docs/artifacts/MAVEN_REPO_ARTIFACTS#github-packages>
     * In kmmbridge, notice mavenPublishArtifacts() tells the plugin to push KMMBridge artifacts to a Maven repo. You then need to define a repo. Rather than do everything manually, you can just call addGithubPackagesRepository(), which will add the correct repo given parameters that are passed in from GitHub Actions.
     */
    mavenPublishArtifacts()
//    spm()
    spm {
        swiftToolsVersion = "5.9"
        platforms {
            iOS("14")
            macOS("13")
        }
    }
}

skie {
    build {
        enableSwiftLibraryEvolution.set(true)
    }
    features {
        group {
            DefaultArgumentInterop.Enabled(true) // or false
        }
        enableSwiftUIObservingPreview = true
    }
}

tasks {
    named<Test>("jvmTest") {
        useJUnitPlatform()
    }
}
you see it’s happened after commented out
//            implementation("co.touchlab.skie:runtime-kotlin:${libs.versions.touchlab.skie.get()}")
built with kmmbridge
t
Thanks, that’s interesting. Do you know if it happens if you build framework without KMMBridge?
i
not sure if it’s related to this it’s will download the version of • https://repo.maven.apache.org/maven2/co/touchlab/skie/runtime-kotlin-macosarm64__kgp_1.9.20/0.6.1/runtime-kotlin-macosarm64__kgp_1.9.20-0.6.1.klib after
Copy code
//            implementation("co.touchlab.skie:runtime-kotlin:${libs.versions.touchlab.skie.get()}")
but after enable
implementation("co.touchlab.skie:runtime-kotlin:${libs.versions.touchlab.skie.get()}")
which download this version https://repo.maven.apache.org/maven2/co/touchlab/skie/runtime-kotlin-macosarm64__kgp_1.9.20/0.8.2/runtime-kotlin-macosarm64__kgp_1.9.20-0.8.2.klib
will try use another tool for build later today.
image.png
f
Hi! I think we run into a similar problem with the SKIE runtime a couple of weeks ago. We have already fixed it, but it’s not released yet. I will release a new SKIE version today so that we can try if this fix works for your project as well.
🚀 1
i
great news! thx
f
Fixed in 0.8.3.
🫡 1
m
I am trying to do the same thing on 0.9.5 but I always get
Any?
in the type.
Copy code
sealed interface Either<out Data, out Failure> {
    data class Success<out Data>(val data: Data) : Either<Data, Nothing>
    data class Failure<out Failure>(val error: Failure) : Either<Nothing, Failure>
}

fun <Data, FailureIn, FailureOut> Either<Data, FailureIn>.mapFailure(transform: (FailureIn) -> FailureOut): Either<Data, FailureOut> = when (this) {
    is Either.Failure -> Either.Failure(transform(error))
    is Either.Success -> Either.Success(data)
}
And then in swift
Copy code
let result = try! await foo.bar().mapFailure { failure in
                failure // this is Any?
            }
Generated code is (seems to lose all type info)
Copy code
extension shared.Either {
   //...

    public func mapFailure(transform: @escaping (Any?) -> Any?) -> shared.Either {
        return shared.EitherKt.mapFailure(self, transform: transform)
    }
}
Of course I do have
Copy code
skie {
    build {
        enableSwiftLibraryEvolution.set(true)
    }
}
Although this is on my
shared
modules, whereas the Either type itself is declared on another module, not sure if that's relevant. I am generating xcframework and static=true. Any ideas what I might be doing wrong? Can you show how the swift wrapper generated by SKIE looked in your case?
Or did is misread it all and it has no way of working out of the box and the idea is to use the Swift Bundler? If so, can you share how it looked in your case for reference? (tried kotlin 2.0.21 and 2.1.0 and skie 0.10.0 as well)
Okay, so for the record: 1. I used sealed interface and generic types are not supported on protocols in obj-c so using sealed interface will lose the type, but sealed class will keep the type. So to fix it I need to change
interface -> class
-
sealed class Either<out Data, out Failure>
2. After having read Kevin's article I realized that
out
variance should work and this is what we have on such Either / Result type, so we're good here (
Either<out Data, out Failure>
) 3. Standard Kotlin's
Result<T>
will not work, because it's a
value
/
inline
class which has limitations of its own 4. In Kotlin I had defined extension functions like
fun <Data, Failure> Either<Data, Failure>.onSuccess(action: (Data) -> Unit): Either<Data, Failure>
. This also strips the type in obj-c. Simply making them regular class functions (
fun onSuccess(action: (Data) -> Unit): Either<Data, Failure>
) retains the type and enables what Ronald has proposed above with his KmpResult. 5. However there are still functions like
fun <DataOut> map(transform: (Data) -> DataOut): Either<DataOut, Failure>
where the
DataOut
type is lost. And it cannot be handled with some clever extensions function on the swift side, because one cannot access generic types via an extension function on obj-c type. So no extension functions for my
Either
type at all 😞. What can be done about it: a. Wrap the whole Either into some SwiftEither and handle the type transformations there. b. Use top-level swift function instead of extensions function (much like SKIE does it with stuff like
skie(result).map...
) c. Just force cast in those situations d. ^ I don't really like any of these. Am wondering if Kotlin 2.1.0 experimental interop does anything helpful here. 6. And swift bundling is not any magical instrument that enables some special interop - it only enabled what would already be possible in Swift - its purpose is different.
f
a. Use top-level swift function instead of extensions function (much like SKIE does it with stuff like
skie(result).map...
)
b. Just force cast in those situations
c. ^ I don’t really like any of these. Am wondering if Kotlin 2.1.0 experimental interop does anything helpful here.
yeah, SKIE has to use the
skie()
function to workaround this limitation. We haven’t found a better way around it for suspend functions. There is a solution it that would work in your case by utilizing Obj-C to Swift bridging like we do for Flows or enums but that is quite complex to setup properly. The Swift Export will not solve that in the foreseeable future as generics are quite far in the roadmap (and it will take a while before it’s production ready)
👍 1
229 Views